diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | include/zix/filesystem.h | 371 | ||||
-rw-r--r-- | include/zix/path.h | 3 | ||||
-rw-r--r-- | include/zix/status.h | 2 | ||||
-rw-r--r-- | include/zix/zix.h | 1 | ||||
-rw-r--r-- | meson.build | 119 | ||||
-rw-r--r-- | meson/suppressions/meson.build | 2 | ||||
-rw-r--r-- | src/errno_status.c | 5 | ||||
-rw-r--r-- | src/filesystem.c | 122 | ||||
-rw-r--r-- | src/posix/filesystem_posix.c | 444 | ||||
-rw-r--r-- | src/posix/system_posix.c | 26 | ||||
-rw-r--r-- | src/status.c | 6 | ||||
-rw-r--r-- | src/system.c | 41 | ||||
-rw-r--r-- | src/system.h | 30 | ||||
-rw-r--r-- | src/win32/filesystem_win32.c | 325 | ||||
-rw-r--r-- | src/win32/system_win32.c | 20 | ||||
-rw-r--r-- | src/zix_config.h | 137 | ||||
-rw-r--r-- | test/.clang-tidy | 1 | ||||
-rw-r--r-- | test/headers/test_headers.c | 1 | ||||
-rw-r--r-- | test/test_filesystem.c | 712 | ||||
-rw-r--r-- | test/test_status.c | 4 |
21 files changed, 2368 insertions, 5 deletions
@@ -17,6 +17,7 @@ Components * `ZixTree`: A binary search tree. * `zix/digest.h`: Digest functions suitable for hashing arbitrary data. + * `zix/filesystem.h`: Functions for working with filesystems. * `zix/path.h`: Functions for working with filesystem paths lexically. Platforms diff --git a/include/zix/filesystem.h b/include/zix/filesystem.h new file mode 100644 index 0000000..7b01c0e --- /dev/null +++ b/include/zix/filesystem.h @@ -0,0 +1,371 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef ZIX_FILESYSTEM_H +#define ZIX_FILESYSTEM_H + +#include "zix/allocator.h" +#include "zix/attributes.h" +#include "zix/status.h" + +#if !(defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64) +# include <stddef.h> +#endif + +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> + +ZIX_BEGIN_DECLS + +/** + @defgroup zix_filesystem Filesystem + @ingroup zix_file_system + @{ +*/ + +/** + @defgroup zix_fs_creation Creation and Removal + @{ +*/ + +/// Options to control filesystem copy operations +typedef enum { + ZIX_COPY_OPTION_NONE = 0U, ///< Report any error + ZIX_COPY_OPTION_OVERWRITE_EXISTING = 1U << 0U, ///< Replace existing file +} ZixCopyOption; + +/// Bitwise OR of ZixCopyOptions values +typedef uint32_t ZixCopyOptions; + +/** + Copy the file at path `src` to path `dst`. + + If supported by the system, a lightweight copy will be made to take + advantage of copy-on-write support in the filesystem. Otherwise, a simple + deep copy will be made. + + @param allocator Allocator used for a memory block for copying if necessary. + @param src Path to source file to copy. + @param dst Path to destination file to create. + @param options Options to control the kind of copy and error conditions. + @return #ZIX_STATUS_SUCCESS if `dst` was successfully created, or an error. +*/ +ZIX_API +ZixStatus +zix_copy_file(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NONNULL src, + const char* ZIX_NONNULL dst, + ZixCopyOptions options); + +/** + Create the directory `dir_path` with all available permissions. + + @return #ZIX_STATUS_SUCCESS if `dir_path` was successfully created, or an + error. +*/ +ZIX_API +ZixStatus +zix_create_directory(const char* ZIX_NONNULL dir_path); + +/** + Create the directory `dir_path` with the permissions of another. + + This is like zix_create_directory(), but will copy the permissions from + another directory. + + @return #ZIX_STATUS_SUCCESS if `dir_path` was successfully created, or an + error. +*/ +ZIX_API +ZixStatus +zix_create_directory_like(const char* ZIX_NONNULL dir_path, + const char* ZIX_NONNULL existing_path); + +/** + Create the directory `dir_path` and any parent directories if necessary. + + @param allocator Allocator used for a temporary path buffer if necessary. + + @param dir_path The path to the deepest directory to create. + + @return #ZIX_STATUS_SUCCESS if all directories in `dir_path` were + successfully created (or already existed), or an error. +*/ +ZIX_API +ZixStatus +zix_create_directories(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NONNULL dir_path); + +/** + Create a hard link at path `link` that points to path `target`. + + @return #ZIX_STATUS_SUCCESS, or an error. +*/ +ZIX_API +ZixStatus +zix_create_hard_link(const char* ZIX_NONNULL target_path, + const char* ZIX_NONNULL link_path); + +/** + Create a symbolic link at path `link` that points to path `target`. + + Note that portable code should use zix_create_directory_symlink() if the + target is a directory, since this function won't work for that on some + systems (like Windows). + + @return #ZIX_STATUS_SUCCESS, or an error. +*/ +ZIX_API +ZixStatus +zix_create_symlink(const char* ZIX_NONNULL target_path, + const char* ZIX_NONNULL link_path); + +/** + Create a symbolic link at path `link` that points to the directory `target`. + + This is a separate function from zix_create_symlink() because some systems + (like Windows) require directory symlinks to be created specially. + + @return #ZIX_STATUS_SUCCESS, or an error. +*/ +ZIX_API +ZixStatus +zix_create_directory_symlink(const char* ZIX_NONNULL target_path, + const char* ZIX_NONNULL link_path); + +/** + Create a unique temporary directory at a given path pattern. + + The last six characters of `pattern` must be "XXXXXX" and will be replaced + with unique characters in the result. + + @param allocator Allocator used for the returned path. + + @param path_pattern A path pattern ending in "XXXXXX". + + @return The path of the created directory, or null. +*/ +ZIX_API +char* ZIX_ALLOCATED +zix_create_temporary_directory(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NONNULL path_pattern); + +/// Remove the file or empty directory at `path` +ZIX_API +ZixStatus +zix_remove(const char* ZIX_NONNULL path); + +/** + @} + @defgroup zix_fs_access Access + @{ +*/ + +/** + Visit every file in the directory at `path`. + + @param path A path to a directory. + + @param data Opaque user data that is passed to `f`. + + @param f A function called on every entry in the directory. The `path` + parameter is always the directory path passed to this function, the `name` + parameter is the name of the directory entry (not its full path). +*/ +ZIX_API +void +zix_dir_for_each(const char* ZIX_NONNULL path, + void* ZIX_NULLABLE data, + void (*ZIX_NONNULL f)(const char* ZIX_NONNULL path, + const char* ZIX_NONNULL name, + void* ZIX_NONNULL data)); + +/** + Return whether the given paths point to files with identical contents. + + @param allocator Allocator used for a memory block for comparison if + necessary. + + @param a_path Path to the first file to compare + + @param b_path Path to the second file to compare + + @return True if the two files have byte-for-byte identical contents. +*/ +ZIX_API +bool +zix_file_equals(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NONNULL a_path, + const char* ZIX_NONNULL b_path); + +/** + @} + @defgroup zix_fs_resolution Resolution + @{ +*/ + +/** + Return `path` as a canonical absolute path to a "real" file. + + This expands all symbolic links, relative references, and removes extra + directory separators. + + Since this function may return null anyway, it accepts a null parameter to + allow easier chaining of path functions when only the final result is + required, for example: + + @code{.c} + char* path = zix_path_join(alloc, "/some/dir", "filename.txt"); + char* canonical = zix_canonical_path(path); + if (canonical) { + // Do something with the canonical path... + } else { + // No canonical path for some reason, we don't care which... + } + @endcode + + @return A new canonical version of `path`, or null if it doesn't exist. +*/ +ZIX_API +char* ZIX_ALLOCATED +zix_canonical_path(ZixAllocator* ZIX_NULLABLE allocator, + const char* ZIX_NULLABLE path); + +/** + @} + @defgroup zix_fs_locking Locking + @{ +*/ + +/** + A mode for locking files. + + The same mode should be used for the lock and the corresponding unlock. +*/ +typedef enum { + ZIX_FILE_LOCK_BLOCK, ///< Block until the operation succeeds + ZIX_FILE_LOCK_TRY, ///< Fail if the operation would block +} ZixFileLockMode; + +/** + Set an advisory exclusive lock on `file`. + + @param file Handle for open file to lock. + @param mode Lock mode. + @return #ZIX_STATUS_SUCCESS if the file was locked, or an error. +*/ +ZIX_API +ZixStatus +zix_file_lock(FILE* ZIX_NONNULL file, ZixFileLockMode mode); + +/** + Remove an advisory exclusive lock on `file`. + + @param file Handle for open file to lock. + @param mode Lock mode. + @return #ZIX_STATUS_SUCCESS if the file was unlocked, or an error. +*/ +ZIX_API +ZixStatus +zix_file_unlock(FILE* ZIX_NONNULL file, ZixFileLockMode mode); + +/** + @} + @defgroup zix_fs_queries Queries + @{ +*/ + +/** + An offset into a file or a file size in bytes. + + This is signed, and may be 64 bits even on 32-bit systems. +*/ +#if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 +typedef int64_t ZixFileOffset; +#else +typedef ptrdiff_t ZixFileOffset; +#endif + +/** + A type of file. + + Note that not all types may be supported, and the system may support + additional types not enumerated here. +*/ +typedef enum { + ZIX_FILE_TYPE_NONE, ///< Non-existent file + ZIX_FILE_TYPE_REGULAR, ///< Regular file + ZIX_FILE_TYPE_DIRECTORY, ///< Directory + ZIX_FILE_TYPE_SYMLINK, ///< Symbolic link + ZIX_FILE_TYPE_BLOCK, ///< Special block file + ZIX_FILE_TYPE_CHARACTER, ///< Special character file + ZIX_FILE_TYPE_FIFO, ///< FIFO + ZIX_FILE_TYPE_SOCKET, ///< Socket + ZIX_FILE_TYPE_UNKNOWN, ///< Existing file with unknown type +} ZixFileType; + +/** + Return the type of a file or directory, resolving symlinks. +*/ +ZIX_API +ZixFileType +zix_file_type(const char* ZIX_NONNULL path); + +/** + Return the type of a file or directory or symlink. + + On Windows, a directory symlink (actually a "reparse point") always appears + as a directory. +*/ +ZIX_API +ZixFileType +zix_symlink_type(const char* ZIX_NONNULL path); + +/** + Return the size of a file. + + Note that the returned value is signed and must be checked for errors. + Non-negative values can be thought of as the "end" offset just past the last + byte. + + @return A non-negative size in bytes, or -1 on error. +*/ +ZIX_API +ZixFileOffset +zix_file_size(const char* ZIX_NONNULL path); + +/** + @} + @defgroup zix_fs_environment Environment + @{ +*/ + +/** + Return the current working directory. + + @param allocator Allocator used for the returned path. +*/ +ZIX_API +char* ZIX_ALLOCATED +zix_current_path(ZixAllocator* ZIX_NULLABLE allocator); + +/** + Return the path to a directory suitable for making temporary files. + + @param allocator Allocator used for the returned path. + + @return A new path to a temporary directory, or null on error. +*/ +ZIX_API +char* ZIX_ALLOCATED +zix_temp_directory_path(ZixAllocator* ZIX_NULLABLE allocator); + +/** + @} + @} +*/ + +ZIX_END_DECLS + +#endif /* ZIX_FILESYSTEM_H */ diff --git a/include/zix/path.h b/include/zix/path.h index 5d3bd60..7a52d31 100644 --- a/include/zix/path.h +++ b/include/zix/path.h @@ -67,7 +67,8 @@ zix_path_preferred(ZixAllocator* ZIX_NULLABLE allocator, replaced with a single "\" on Windows, and a single "/" everwhere else). Note that this function doesn't access the filesystem, so won't do anything - like case normalization or symbolic link dereferencing. + like case normalization or symbolic link dereferencing. For that, use + zix_canonical_path(). */ ZIX_API char* ZIX_ALLOCATED diff --git a/include/zix/status.h b/include/zix/status.h index 0e95f43..dfd6412 100644 --- a/include/zix/status.h +++ b/include/zix/status.h @@ -28,6 +28,8 @@ typedef enum { ZIX_STATUS_OVERFLOW, ///< Overflow ZIX_STATUS_NOT_SUPPORTED, ///< Not supported ZIX_STATUS_UNAVAILABLE, ///< Resource unavailable + ZIX_STATUS_NO_SPACE, ///< Out of storage space + ZIX_STATUS_MAX_LINKS, ///< Too many links } ZixStatus; /// Return a string describing a status code diff --git a/include/zix/zix.h b/include/zix/zix.h index a91b6f8..b144cee 100644 --- a/include/zix/zix.h +++ b/include/zix/zix.h @@ -58,6 +58,7 @@ @{ */ +#include "zix/filesystem.h" #include "zix/path.h" /** diff --git a/meson.build b/meson.build index bf082b9..5f8d078 100644 --- a/meson.build +++ b/meson.build @@ -43,8 +43,25 @@ platform_c_args = [] no_posix = get_option('posix').disabled() or host_machine.system() == 'windows' if no_posix platform_c_args += ['-DZIX_NO_POSIX'] +elif host_machine.system() == 'darwin' + platform_c_args += [ + '-D_DARWIN_C_SOURCE', + ] +elif host_machine.system() in ['gnu', 'linux'] + platform_c_args += [ + '-D_GNU_SOURCE', + '-D_POSIX_C_SOURCE=200809L', + '-D_XOPEN_SOURCE=600', + ] +elif host_machine.system() in ['dragonfly', 'freebsd', 'netbsd', 'openbsd'] + platform_c_args += [ + '-D_BSD_SOURCE', + ] else - platform_c_args += ['-D_POSIX_C_SOURCE=200809L'] + platform_c_args += [ + '-D_POSIX_C_SOURCE=200809L', + '-D_XOPEN_SOURCE=600', + ] endif # Check for platform features with the build system @@ -53,36 +70,113 @@ if get_option('checks') int main(void) { struct timespec t; return clock_gettime(CLOCK_MONOTONIC, &t); } ''' + clonefile_code = '''#include <sys/attr.h> +#include <sys/clonefile.h> +int main(void) { return clonefile("/src", "/dst", 0); }''' + + copy_file_range_code = '''#include <unistd.h> +int main(void) { return copy_file_range(0, NULL, 1, NULL, 0U, 0U); }''' + + CreateSymbolicLink_code = '''#include <windows.h> +int main(void) { + return CreateSymbolicLink( + "l", "t", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); +}''' + + fileno_code = '''#include <stdio.h> +int main(void) { return fileno(stdin); }''' + + flock_code = '''#include <sys/file.h> +int main(void) { return flock(0, 0); }''' + + lstat_code = '''#include <sys/stat.h> +int main(void) { struct stat s; return lstat("/", &s); }''' + mlock_code = '''#include <sys/mman.h> int main(void) { return mlock(0, 0); }''' + pathconf_code = '''#include <unistd.h> +int main(void) { return pathconf("/", _PC_PATH_MAX) > 0L; }''' + + posix_fadvise_code = '''#include <fcntl.h> +int main(void) { posix_fadvise(0, 0, 4096, POSIX_FADV_SEQUENTIAL); }''' + posix_memalign_code = '''#include <stdlib.h> int main(void) { void* mem; posix_memalign(&mem, 8, 8); }''' + realpath_code = '''#include <stdlib.h> +int main(void) { return realpath("/", NULL) != NULL; }''' + sem_timedwait_code = '''#include <semaphore.h> #include <time.h> int main(void) { sem_t s; struct timespec t; return sem_timedwait(&s, &t); }''' + sysconf_code = '''#include <unistd.h> +int main(void) { return sysconf(_SC_PAGE_SIZE) > 0L; }''' + platform_c_args += [ '-DZIX_NO_DEFAULT_CONFIG', '-DHAVE_CLOCK_GETTIME=@0@'.format( cc.links(clock_gettime_code, args: platform_c_args, name: 'clock_gettime').to_int()), + '-DHAVE_CLONEFILE=@0@'.format( + (host_machine.system() == 'darwin' and + cc.links(clonefile_code, + args: platform_c_args, + name: 'clonefile')).to_int()), + '-DHAVE_COPY_FILE_RANGE=@0@'.format( + (host_machine.system() not in ['darwin', 'windows'] and + cc.links(copy_file_range_code, + args: platform_c_args, + name: 'copy_file_range')).to_int()), + '-DHAVE_CREATESYMBOLICLINK=@0@'.format( + (host_machine.system() == 'windows' and + cc.links(CreateSymbolicLink_code, + args: platform_c_args, + name: 'CreateSymbolicLink')).to_int()), + '-DHAVE_FILENO=@0@'.format( + cc.links(fileno_code, + args: platform_c_args, + name: 'fileno').to_int()), + '-DHAVE_FLOCK=@0@'.format( + (host_machine.system() != 'windows' and + cc.links(flock_code, + args: platform_c_args, + name: 'flock')).to_int()), '-DHAVE_MLOCK=@0@'.format( cc.links(mlock_code, args: platform_c_args, name: 'mlock').to_int()), + '-DHAVE_PATHCONF=@0@'.format( + (host_machine.system() != 'windows' and + cc.links(pathconf_code, + args: platform_c_args, + name: 'pathconf')).to_int()), + '-DHAVE_POSIX_FADVISE=@0@'.format( + cc.links(posix_fadvise_code, + args: platform_c_args, + name: 'posix_fadvise').to_int()), '-DHAVE_POSIX_MEMALIGN=@0@'.format( cc.links(posix_memalign_code, args: platform_c_args, name: 'posix_memalign').to_int()), + '-DHAVE_REALPATH=@0@'.format( + (host_machine.system() != 'windows' and + cc.links(realpath_code, + args: platform_c_args, + name: 'realpath')).to_int()), '-DHAVE_SEM_TIMEDWAIT=@0@'.format( (host_machine.system() not in ['darwin', 'windows'] and cc.links(sem_timedwait_code, args: platform_c_args, dependencies: [thread_dep], name: 'sem_timedwait')).to_int()), + '-DHAVE_SYSCONF=@0@'.format( + (host_machine.system() != 'windows' and + cc.links(sysconf_code, + args: platform_c_args, + name: 'sysconf')).to_int()), ] endif @@ -99,6 +193,7 @@ c_headers = files( 'include/zix/btree.h', 'include/zix/bump_allocator.h', 'include/zix/digest.h', + 'include/zix/filesystem.h', 'include/zix/function_types.h', 'include/zix/hash.h', 'include/zix/path.h', @@ -118,11 +213,13 @@ sources = files( 'src/bump_allocator.c', 'src/digest.c', 'src/errno_status.c', + 'src/filesystem.c', 'src/hash.c', 'src/path.c', 'src/ring.c', 'src/status.c', 'src/string_view.c', + 'src/system.c', 'src/tree.c', ) @@ -130,17 +227,23 @@ if thread_dep.found() if host_machine.system() == 'darwin' sources += files( 'src/darwin/sem_darwin.c', + 'src/posix/filesystem_posix.c', + 'src/posix/system_posix.c', 'src/posix/thread_posix.c', ) elif host_machine.system() == 'windows' sources += files( + 'src/win32/filesystem_win32.c', 'src/win32/sem_win32.c', + 'src/win32/system_win32.c', 'src/win32/thread_win32.c', ) else sources += files( + 'src/posix/filesystem_posix.c', 'src/posix/sem_posix.c', + 'src/posix/system_posix.c', 'src/posix/thread_posix.c', ) endif @@ -282,6 +385,20 @@ if not get_option('tests').disabled() ) endforeach + test( + 'test_filesystem', + executable( + 'test_filesystem', + files('test/test_filesystem.c'), + c_args: c_suppressions + program_c_args, + dependencies: [zix_dep], + include_directories: include_dirs, + link_args: program_link_args, + ), + args: files('README.md'), + timeout: 120, + ) + if thread_dep.found() foreach test : threaded_tests sources = common_test_sources + files('test/@0@.c'.format(test)) diff --git a/meson/suppressions/meson.build b/meson/suppressions/meson.build index 06a0821..2f07e61 100644 --- a/meson/suppressions/meson.build +++ b/meson/suppressions/meson.build @@ -38,6 +38,7 @@ if is_variable('cc') if host_machine.system() == 'windows' c_suppressions += [ '-Wno-format', + '-Wno-suggest-attribute=const', '-Wno-suggest-attribute=format', '-Wno-suggest-attribute=pure', ] @@ -56,6 +57,7 @@ if is_variable('cc') '/wd4777', # format string and argument mismatch '/wd4800', # implicit conversion to bool '/wd4820', # padding added after construct + '/wd4996', # POSIX name for this item is deprecated '/wd5045', # will insert Spectre mitigation for memory load ] endif diff --git a/src/errno_status.c b/src/errno_status.c index 142d456..4922b56 100644 --- a/src/errno_status.c +++ b/src/errno_status.c @@ -28,11 +28,16 @@ zix_errno_status(const int e) {EAGAIN, ZIX_STATUS_UNAVAILABLE}, {EEXIST, ZIX_STATUS_EXISTS}, {EINVAL, ZIX_STATUS_BAD_ARG}, + {EMLINK, ZIX_STATUS_MAX_LINKS}, {ENOENT, ZIX_STATUS_NOT_FOUND}, {ENOMEM, ZIX_STATUS_NO_MEM}, + {ENOSPC, ZIX_STATUS_NO_SPACE}, {ENOSYS, ZIX_STATUS_NOT_SUPPORTED}, {EPERM, ZIX_STATUS_BAD_PERMS}, {ETIMEDOUT, ZIX_STATUS_TIMEOUT}, +#ifdef ENOTSUP + {ENOTSUP, ZIX_STATUS_NOT_SUPPORTED}, +#endif {0, ZIX_STATUS_ERROR}, // Fallback mapping }; diff --git a/src/filesystem.c b/src/filesystem.c new file mode 100644 index 0000000..c3e8f10 --- /dev/null +++ b/src/filesystem.c @@ -0,0 +1,122 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "zix/filesystem.h" + +#include "index_range.h" +#include "path_iter.h" +#include "system.h" + +#include "zix/allocator.h" +#include "zix/status.h" + +#ifdef _WIN32 +# include <direct.h> +# include <io.h> +#else +# include <unistd.h> +#endif + +#include <fcntl.h> +#include <sys/stat.h> + +#include <errno.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +ZixStatus +zix_create_directories(ZixAllocator* const allocator, + const char* const dir_path) +{ + if (!dir_path[0]) { + return ZIX_STATUS_BAD_ARG; + } + + // Allocate a working copy of the path to chop along the way + const size_t path_len = strlen(dir_path); + char* const path = (char*)zix_malloc(allocator, path_len + 1U); + memcpy(path, dir_path, path_len + 1U); + + // Start at the root directory (past any name) + ZixPathIter p = zix_path_begin(path); + while (p.state < ZIX_PATH_FILE_NAME) { + p = zix_path_next(path, p); + } + + // Create each directory down the path + ZixStatus st = ZIX_STATUS_SUCCESS; + while (p.state != ZIX_PATH_END) { + const char old_end = path[p.range.end]; + + path[p.range.end] = '\0'; + if (zix_file_type(path) != ZIX_FILE_TYPE_DIRECTORY) { + if ((st = zix_create_directory(path))) { + break; + } + } + + path[p.range.end] = old_end; + p = zix_path_next(path, p); + } + + zix_free(allocator, path); + return st; +} + +ZixFileOffset +zix_file_size(const char* const path) +{ + struct stat sb; + return stat(path, &sb) ? (off_t)0 : sb.st_size; +} + +bool +zix_file_equals(ZixAllocator* const allocator, + const char* const path_a, + const char* const path_b) +{ + if (!strcmp(path_a, path_b)) { + return true; // Paths match + } + + errno = 0; + + // Open files and get file information + const int fd_a = zix_system_open_fd(path_a, O_RDONLY, 0); + const int fd_b = zix_system_open_fd(path_b, O_RDONLY, 0); + struct stat stat_a; + struct stat stat_b; + if (fd_a < 0 || fd_b < 0 || fstat(fd_a, &stat_a) || fstat(fd_b, &stat_b)) { + zix_system_close_fds(fd_b, fd_a); + return false; + } + + bool match = false; + if (stat_a.st_dev == stat_b.st_dev && stat_a.st_ino && stat_b.st_ino && + stat_a.st_ino == stat_b.st_ino) { + match = true; // Fast path: paths refer to the same file + } else if (stat_a.st_size == stat_b.st_size) { + // Slow path: files have equal size, compare contents + const uint32_t size = zix_system_page_size(); + void* const page_a = zix_aligned_alloc(allocator, size, size); + void* const page_b = zix_aligned_alloc(allocator, size, size); + + if (page_a && page_b) { + match = true; + for (ZixSystemCountReturn n = 0; (n = read(fd_a, page_a, size)) > 0;) { + if (read(fd_b, page_b, size) != n || + !!memcmp(page_a, page_b, (size_t)n)) { + match = false; + break; + } + } + } + + zix_aligned_free(allocator, page_b); + zix_aligned_free(allocator, page_a); + } + + return !zix_system_close_fds(fd_b, fd_a) && match; +} diff --git a/src/posix/filesystem_posix.c b/src/posix/filesystem_posix.c new file mode 100644 index 0000000..0360403 --- /dev/null +++ b/src/posix/filesystem_posix.c @@ -0,0 +1,444 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "zix/filesystem.h" + +#include "../errno_status.h" +#include "../system.h" +#include "../zix_config.h" + +#include "zix/allocator.h" +#include "zix/status.h" + +#if USE_FLOCK && USE_FILENO +# include <sys/file.h> +#endif + +#if USE_CLONEFILE +# include <sys/attr.h> +# include <sys/clonefile.h> +#endif + +#if USE_REALPATH +# include <limits.h> +#endif + +#include <dirent.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <errno.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifndef MAX +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +static inline ZixStatus +zix_posix_status(const int rc) +{ + return rc ? zix_errno_status(errno) : ZIX_STATUS_SUCCESS; +} + +static uint32_t +zix_get_block_size(const struct stat s1, const struct stat s2) +{ + const blksize_t b1 = s1.st_blksize; + const blksize_t b2 = s2.st_blksize; + + return (b1 > 0 && b2 > 0) ? (uint32_t)MAX(b1, b2) : 4096U; +} + +static ZixStatus +finish_copy(const int dst_fd, const int src_fd, const ZixStatus status) +{ + const ZixStatus st0 = zix_posix_status(dst_fd >= 0 ? fdatasync(dst_fd) : 0); + const ZixStatus st1 = zix_system_close_fds(dst_fd, src_fd); + + return status ? status : st0 ? st0 : st1; +} + +static char* +copy_path(ZixAllocator* const allocator, + const char* const path, + const size_t length) +{ + char* result = NULL; + + if (path) { + if ((result = (char*)zix_calloc(allocator, length + 1U, 1U))) { + memcpy(result, path, length + 1U); + } + } + + return result; +} + +#if !defined(PATH_MAX) && USE_PATHCONF + +static size_t +max_path_size(void) +{ + const long path_max = pathconf(path, _PC_PATH_MAX); + return (path_max > 0) ? (size_t)path_max : zix_system_page_size(); +} + +#elif !defined(PATH_MAX) + +static size_t +max_path_size(void) +{ + return zix_system_page_size(); +} + +#endif + +#if USE_CLONEFILE + +static ZixStatus +zix_clonefile(const char* const src, + const char* const dst, + const ZixCopyOptions options) +{ + errno = 0; + + ZixStatus st = ZIX_STATUS_SUCCESS; + const ZixFileType dst_type = zix_file_type(dst); + const bool overwrite = (options == ZIX_COPY_OPTION_OVERWRITE_EXISTING); + if (overwrite && dst_type == ZIX_FILE_TYPE_REGULAR) { + st = zix_remove(dst); + } else if (dst_type != ZIX_FILE_TYPE_NONE) { + st = ZIX_STATUS_NOT_SUPPORTED; + } + + return st ? st : zix_posix_status(clonefile(src, dst, 0)); +} + +#endif + +#if USE_COPY_FILE_RANGE + +static ZixStatus +zix_copy_file_range(const int src_fd, const int dst_fd, const size_t size) +{ + errno = 0; + + size_t remaining = size; + ssize_t r = 0; + while (remaining > 0 && + (r = copy_file_range(src_fd, NULL, dst_fd, NULL, remaining, 0U)) > 0) { + remaining -= (size_t)r; + } + + return (r >= 0) ? ZIX_STATUS_SUCCESS + : zix_errno_status( + (errno == EXDEV || errno == EINVAL) ? ENOSYS : errno); +} + +#endif + +static ZixStatus +copy_blocks(const int src_fd, + const int dst_fd, + void* const block, + const size_t block_size) +{ + ssize_t n_read = 0; + while ((n_read = read(src_fd, block, block_size)) > 0) { + if (write(dst_fd, block, (size_t)n_read) != n_read) { + return zix_errno_status(errno); + } + } + + return ZIX_STATUS_SUCCESS; +} + +ZixStatus +zix_copy_file(ZixAllocator* const allocator, + const char* const src, + const char* const dst, + const ZixCopyOptions options) +{ + ZixStatus st = ZIX_STATUS_SUCCESS; + (void)st; + +#if USE_CLONEFILE + // Try to copy via the kernel on MacOS to take advantage of CoW + st = zix_clonefile(src, dst, options); + if (st != ZIX_STATUS_NOT_SUPPORTED) { + return st; + } +#endif + + // Open source file and get its status + const int src_fd = zix_system_open_fd(src, O_RDONLY, 0); + struct stat src_stat; + if (src_fd < 0 || fstat(src_fd, &src_stat)) { + return finish_copy(-1, src_fd, zix_errno_status(errno)); + } + + // Fail if the source is not a regular file (since we need a size) + if (!S_ISREG(src_stat.st_mode)) { + return finish_copy(-1, src_fd, ZIX_STATUS_BAD_ARG); + } + + // Open a new destination file + const bool overwrite = (options == ZIX_COPY_OPTION_OVERWRITE_EXISTING); + const int dst_flags = O_WRONLY | O_CREAT | (overwrite ? O_TRUNC : O_EXCL); + const int dst_fd = zix_system_open_fd(dst, dst_flags, 0644); + struct stat dst_stat; + if (dst_fd < 0 || fstat(dst_fd, &dst_stat)) { + return finish_copy(dst_fd, src_fd, zix_errno_status(errno)); + } + +#if USE_COPY_FILE_RANGE + // Try to copy via the kernel on Linux/BSD to take advantage of CoW + st = zix_copy_file_range(src_fd, dst_fd, (size_t)src_stat.st_size); + if (st != ZIX_STATUS_NOT_SUPPORTED) { + return finish_copy(dst_fd, src_fd, st); + } +#endif + + // Set sequential hints so the kernel can optimize the page cache +#if USE_POSIX_FADVISE + posix_fadvise(src_fd, 0, src_stat.st_size, POSIX_FADV_SEQUENTIAL); + posix_fadvise(dst_fd, 0, src_stat.st_size, POSIX_FADV_SEQUENTIAL); +#endif + + errno = 0; + + // Allocate a block for copying + const size_t align = zix_system_page_size(); + const uint32_t block_size = zix_get_block_size(src_stat, dst_stat); + void* const block = zix_aligned_alloc(allocator, align, block_size); + + // Fall back to using a small stack buffer if allocation is unavailable + char stack_buf[512]; + void* const buffer = block ? block : stack_buf; + const size_t buffer_size = block ? block_size : sizeof(stack_buf); + + // Copy file content one buffer at a time + st = copy_blocks(src_fd, dst_fd, buffer, buffer_size); + + zix_aligned_free(NULL, block); + return finish_copy(dst_fd, src_fd, st); +} + +ZixStatus +zix_create_directory(const char* const dir_path) +{ + return (!dir_path[0]) ? ZIX_STATUS_BAD_ARG + : zix_posix_status(mkdir(dir_path, 0777)); +} + +ZixStatus +zix_create_directory_like(const char* const dir_path, + const char* const existing_path) +{ + struct stat sb; + return !dir_path[0] ? ZIX_STATUS_BAD_ARG + : stat(existing_path, &sb) + ? zix_errno_status(errno) + : zix_posix_status(mkdir(dir_path, sb.st_mode)); +} + +ZixStatus +zix_create_hard_link(const char* const target_path, const char* const link_path) +{ + return zix_posix_status(link(target_path, link_path)); +} + +ZixStatus +zix_create_symlink(const char* const target_path, const char* const link_path) +{ + return zix_posix_status(symlink(target_path, link_path)); +} + +ZixStatus +zix_create_directory_symlink(const char* const target_path, + const char* const link_path) +{ + return zix_create_symlink(target_path, link_path); +} + +char* +zix_create_temporary_directory(ZixAllocator* const allocator, + const char* const path_pattern) +{ + const size_t length = strlen(path_pattern); + char* const result = (char*)zix_calloc(allocator, length + 1U, 1U); + if (result) { + memcpy(result, path_pattern, length + 1U); + if (!mkdtemp(result)) { + zix_free(allocator, result); + return NULL; + } + } + + return result; +} + +ZixStatus +zix_remove(const char* const path) +{ + return zix_posix_status(remove(path)); +} + +void +zix_dir_for_each(const char* const path, + void* const data, + void (*const f)(const char* path, + const char* name, + void* data)) +{ + DIR* dir = opendir(path); + if (dir) { + // NOLINTNEXTLINE(concurrency-mt-unsafe) + for (struct dirent* entry = NULL; (entry = readdir(dir));) { + if (!!strcmp(entry->d_name, ".") && !!strcmp(entry->d_name, "..")) { + f(path, entry->d_name, data); + } + } + closedir(dir); + } +} + +char* +zix_canonical_path(ZixAllocator* const allocator, const char* const path) +{ + if (!path) { + return NULL; + } + +#if USE_REALPATH && defined(PATH_MAX) + // Some POSIX systems have a static PATH_MAX so we can resolve on the stack + char buffer[PATH_MAX] = {0}; + char* const canonical = realpath(path, buffer); + if (canonical) { + return copy_path(allocator, buffer, strlen(buffer)); + } + +#elif USE_REALPATH && USE_PATHCONF + // Others don't so we have to query PATH_MAX at runtime to allocate the result + const size_t size = max_path_size(path); + char* const buffer = (char*)zix_calloc(allocator, size, 1); + char* const canonical = realpath(path, buffer); + if (canonical) { + return canonical; + } + + zix_free(allocator, buffer); +#endif + + return NULL; +} + +ZixStatus +zix_file_lock(FILE* const file, const ZixFileLockMode mode) +{ +#if !defined(__EMSCRIPTEN__) && USE_FLOCK && USE_FILENO + return zix_posix_status( + flock(fileno(file), + (mode == ZIX_FILE_LOCK_BLOCK) ? LOCK_EX : (LOCK_EX | LOCK_NB))); + +#else + (void)file; + (void)mode; + return ZIX_STATUS_NOT_SUPPORTED; +#endif +} + +ZixStatus +zix_file_unlock(FILE* const file, const ZixFileLockMode mode) +{ +#if !defined(__EMSCRIPTEN__) && USE_FLOCK && USE_FILENO + return zix_posix_status( + flock(fileno(file), + (mode == ZIX_FILE_LOCK_BLOCK) ? LOCK_UN : (LOCK_UN | LOCK_NB))); + +#else + (void)file; + (void)mode; + return ZIX_STATUS_NOT_SUPPORTED; +#endif +} + +static ZixFileType +stat_file_type(const struct stat sb) +{ + typedef struct { + unsigned mask; + ZixFileType type; + } Mapping; + + static const Mapping map[] = { + {S_IFREG, ZIX_FILE_TYPE_REGULAR}, + {S_IFDIR, ZIX_FILE_TYPE_DIRECTORY}, + {S_IFLNK, ZIX_FILE_TYPE_SYMLINK}, + {S_IFBLK, ZIX_FILE_TYPE_BLOCK}, + {S_IFCHR, ZIX_FILE_TYPE_CHARACTER}, + {S_IFIFO, ZIX_FILE_TYPE_FIFO}, + {S_IFSOCK, ZIX_FILE_TYPE_SOCKET}, + {0U, ZIX_FILE_TYPE_UNKNOWN}, + }; + + const unsigned mask = (unsigned)sb.st_mode & (unsigned)S_IFMT; + unsigned m = 0U; + while (map[m].mask && map[m].mask != mask) { + ++m; + } + + return map[m].type; +} + +ZixFileType +zix_file_type(const char* const path) +{ + struct stat sb; + return stat(path, &sb) ? ZIX_FILE_TYPE_NONE : stat_file_type(sb); +} + +ZixFileType +zix_symlink_type(const char* const path) +{ + struct stat sb; + return lstat(path, &sb) ? ZIX_FILE_TYPE_NONE : stat_file_type(sb); +} + +char* +zix_temp_directory_path(ZixAllocator* const allocator) +{ + const char* tmpdir = getenv("TMPDIR"); // NOLINT(concurrency-mt-unsafe) + + tmpdir = tmpdir ? tmpdir : "/tmp"; + + return copy_path(allocator, tmpdir, strlen(tmpdir)); +} + +char* +zix_current_path(ZixAllocator* const allocator) +{ +#if defined(PATH_MAX) + // Some POSIX systems have a static PATH_MAX so we can store it on the stack + char buffer[PATH_MAX] = {0}; + return copy_path(allocator, getcwd(buffer, PATH_MAX), strlen(buffer)); + +#elif USE_PATHCONF + // Others don't so we have to query PATH_MAX at runtime to allocate the result + const size_t size = max_path_size(); + char* const buffer = (char*)zix_calloc(allocator, size, 1); + char* const current = getcwd(buffer, size); + if (current) { + return current; + } + + zix_free(allocator, buffer); +#endif + + return NULL; +} diff --git a/src/posix/system_posix.c b/src/posix/system_posix.c new file mode 100644 index 0000000..4c37315 --- /dev/null +++ b/src/posix/system_posix.c @@ -0,0 +1,26 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "../system.h" +#include "../zix_config.h" + +#include <unistd.h> + +#include <stdint.h> + +#if defined(PAGE_SIZE) +# define ZIX_DEFAULT_PAGE_SIZE PAGE_SIZE +#else +# define ZIX_DEFAULT_PAGE_SIZE 4096U +#endif + +uint32_t +zix_system_page_size(void) +{ +#if USE_SYSCONF + const long r = sysconf(_SC_PAGE_SIZE); + return r > 0L ? (uint32_t)r : ZIX_DEFAULT_PAGE_SIZE; +#else + return (uint32_t)ZIX_DEFAULT_PAGE_SIZE; +#endif +} diff --git a/src/status.c b/src/status.c index 4b06b97..e3d8b99 100644 --- a/src/status.c +++ b/src/status.c @@ -1,4 +1,4 @@ -// Copyright 2014-2020 David Robillard <d@drobilla.net> +// Copyright 2014-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #include "zix/status.h" @@ -31,6 +31,10 @@ zix_strerror(const ZixStatus status) return "Not supported"; case ZIX_STATUS_UNAVAILABLE: return "Resource unavailable"; + case ZIX_STATUS_NO_SPACE: + return "Out of storage space"; + case ZIX_STATUS_MAX_LINKS: + return "Too many links"; } return "Unknown error"; } diff --git a/src/system.c b/src/system.c new file mode 100644 index 0000000..eab568b --- /dev/null +++ b/src/system.c @@ -0,0 +1,41 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "system.h" + +#include "errno_status.h" + +#include "zix/status.h" + +#ifdef _WIN32 +# include <io.h> +#else +# include <unistd.h> +#endif + +#include <errno.h> +#include <fcntl.h> + +int +zix_system_open_fd(const char* const path, const int flags, const mode_t mode) +{ +#ifdef O_CLOEXEC + return open(path, flags | O_CLOEXEC, mode); // NOLINT(hicpp-signed-bitwise) +#else + return open(path, flags, mode); +#endif +} + +ZixStatus +zix_system_close_fds(const int fd1, const int fd2) +{ + // Careful: we need to always close both files, but catch errno at any point + + const ZixStatus st0 = zix_errno_status(errno); + const int r1 = fd1 >= 0 ? close(fd1) : 0; + const ZixStatus st1 = r1 ? ZIX_STATUS_SUCCESS : zix_errno_status(errno); + const int r2 = fd2 >= 0 ? close(fd2) : 0; + const ZixStatus st2 = r2 ? ZIX_STATUS_SUCCESS : zix_errno_status(errno); + + return st0 ? st0 : st1 ? st1 : st2; +} diff --git a/src/system.h b/src/system.h new file mode 100644 index 0000000..ca55161 --- /dev/null +++ b/src/system.h @@ -0,0 +1,30 @@ +// Copyright 2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef ZIX_SYSTEM_H +#define ZIX_SYSTEM_H + +#include "zix/status.h" + +#include <stdint.h> +#include <sys/types.h> + +#ifdef _WIN32 +typedef int ZixSystemCountReturn; +# ifndef __GNUC__ +typedef int mode_t; +# endif +#else +typedef ssize_t ZixSystemCountReturn; +#endif + +uint32_t +zix_system_page_size(void); + +int +zix_system_open_fd(const char* path, int flags, mode_t mode); + +ZixStatus +zix_system_close_fds(int fd1, int fd2); + +#endif // ZIX_SYSTEM_H diff --git a/src/win32/filesystem_win32.c b/src/win32/filesystem_win32.c new file mode 100644 index 0000000..3e9b7a6 --- /dev/null +++ b/src/win32/filesystem_win32.c @@ -0,0 +1,325 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#define _WIN32_WINNT 0x0600 // Vista + +#include "zix/bump_allocator.h" +#include "zix/filesystem.h" + +#include "../errno_status.h" +#include "../zix_config.h" + +#include "zix/allocator.h" +#include "zix/path.h" +#include "zix/status.h" + +#include <direct.h> +#include <fcntl.h> +#include <io.h> +#include <limits.h> +#include <sys/stat.h> +#include <windows.h> + +#include <errno.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static inline ZixStatus +zix_winerror_status(const DWORD e) +{ + switch (e) { + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ZIX_STATUS_NO_MEM; + case ERROR_SEM_TIMEOUT: + return ZIX_STATUS_TIMEOUT; + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return ZIX_STATUS_NOT_FOUND; + case ERROR_BUFFER_OVERFLOW: + return ZIX_STATUS_OVERFLOW; + case ERROR_DISK_FULL: + return ZIX_STATUS_NO_SPACE; + case ERROR_ALREADY_EXISTS: + case ERROR_FILE_EXISTS: + return ZIX_STATUS_EXISTS; + case ERROR_PRIVILEGE_NOT_HELD: + return ZIX_STATUS_BAD_PERMS; + case ERROR_LOCK_VIOLATION: + return ZIX_STATUS_UNAVAILABLE; + } + + return ZIX_STATUS_ERROR; +} + +static inline ZixStatus +zix_windows_status(const bool success) +{ + return success ? ZIX_STATUS_SUCCESS : zix_winerror_status(GetLastError()); +} + +ZixStatus +zix_copy_file(ZixAllocator* const allocator, + const char* const src, + const char* const dst, + const ZixCopyOptions options) +{ + (void)allocator; + + return zix_windows_status( + CopyFile(src, dst, !(options & ZIX_COPY_OPTION_OVERWRITE_EXISTING))); +} + +char* +zix_create_temporary_directory(ZixAllocator* const allocator, + const char* const path_pattern) +{ + static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + static const int n_chars = sizeof(chars) - 1; + + // Ensure that the pattern ends with "XXXXXX" + const size_t length = strlen(path_pattern); + if (length < 7 || strcmp(path_pattern + length - 6, "XXXXXX")) { + errno = EINVAL; + return NULL; + } + + // Allocate a result to manipulate as we search for paths + char* const result = (char*)zix_calloc(allocator, length + 1U, 1U); + if (!result) { + return NULL; + } + + // Repeatedly try creating a directory with random suffixes + memcpy(result, path_pattern, length + 1U); + char* const suffix = result + length - 6U; + for (unsigned attempt = 0U; attempt < 128U; ++attempt) { + for (unsigned i = 0U; i < 6U; ++i) { + suffix[i] = chars[rand() % n_chars]; + } + + if (!_mkdir(result)) { + return result; + } + } + + zix_free(allocator, result); + return NULL; +} + +ZixStatus +zix_remove(const char* const path) +{ + return ((zix_file_type(path) == ZIX_FILE_TYPE_DIRECTORY) + ? zix_windows_status(RemoveDirectory(path)) + : remove(path) ? zix_errno_status(errno) + : ZIX_STATUS_SUCCESS); +} + +void +zix_dir_for_each(const char* const path, + void* const data, + void (*const f)(const char* path, + const char* name, + void* data)) +{ + const size_t path_len = strlen(path); + char pat[MAX_PATH + 2U]; + memcpy(pat, path, path_len + 1U); + pat[path_len] = '\\'; + pat[path_len + 1U] = '*'; + pat[path_len + 2U] = '\0'; + + WIN32_FIND_DATA fd; + HANDLE fh = FindFirstFile(pat, &fd); + if (fh != INVALID_HANDLE_VALUE) { + do { + if (!!strcmp(fd.cFileName, ".") && !!strcmp(fd.cFileName, "..")) { + f(path, fd.cFileName, data); + } + } while (FindNextFile(fh, &fd)); + } + FindClose(fh); +} + +ZixStatus +zix_file_lock(FILE* const file, const ZixFileLockMode mode) +{ + HANDLE handle = (HANDLE)_get_osfhandle(fileno(file)); + OVERLAPPED overlapped = {0}; + + const DWORD flags = + (LOCKFILE_EXCLUSIVE_LOCK | + (mode == ZIX_FILE_LOCK_TRY ? LOCKFILE_FAIL_IMMEDIATELY : 0)); + + return zix_windows_status( + LockFileEx(handle, flags, 0, UINT32_MAX, UINT32_MAX, &overlapped)); +} + +ZixStatus +zix_file_unlock(FILE* const file, const ZixFileLockMode mode) +{ + (void)mode; + + HANDLE handle = (HANDLE)_get_osfhandle(fileno(file)); + OVERLAPPED overlapped = {0}; + + return zix_windows_status( + UnlockFileEx(handle, 0, UINT32_MAX, UINT32_MAX, &overlapped)); +} + +char* +zix_canonical_path(ZixAllocator* const allocator, const char* const path) +{ + char full[MAX_PATH] = {0}; + if (!path || !GetFullPathName(path, MAX_PATH, full, NULL)) { + return NULL; + } + + const HANDLE h = + CreateFile(full, + FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + const DWORD flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS; + const DWORD final_size = GetFinalPathNameByHandle(h, NULL, 0U, flags); + if (!final_size) { + CloseHandle(h); + return NULL; + } + + char* const final = (char*)zix_calloc(allocator, final_size + 1U, 1U); + if (final && !GetFinalPathNameByHandle(h, final, final_size + 1U, flags)) { + zix_free(allocator, final); + CloseHandle(h); + return NULL; + } + + CloseHandle(h); + return final; +} + +static ZixFileType +attrs_file_type(const DWORD attrs) +{ + if (attrs == INVALID_FILE_ATTRIBUTES) { + return ZIX_FILE_TYPE_NONE; + } + + if (attrs & FILE_ATTRIBUTE_DIRECTORY) { + return ZIX_FILE_TYPE_DIRECTORY; + } + + if (attrs & FILE_ATTRIBUTE_REPARSE_POINT) { + return ZIX_FILE_TYPE_SYMLINK; + } + + if (attrs & (FILE_ATTRIBUTE_DEVICE)) { + return ZIX_FILE_TYPE_UNKNOWN; + } + + return ZIX_FILE_TYPE_REGULAR; +} + +ZixFileType +zix_file_type(const char* const path) +{ + const ZixFileType type = attrs_file_type(GetFileAttributes(path)); + if (type != ZIX_FILE_TYPE_SYMLINK) { + return type; + } + + // Resolve symlink to find the canonical type + char buf[MAX_PATH]; + ZixBumpAllocator allocator = zix_bump_allocator(sizeof(buf), buf); + char* const canonical = zix_canonical_path(&allocator.base, path); + return zix_file_type(canonical); +} + +ZixFileType +zix_symlink_type(const char* const path) +{ + return attrs_file_type(GetFileAttributes(path)); +} + +ZixStatus +zix_create_directory(const char* const dir_path) +{ + return (!dir_path[0]) ? ZIX_STATUS_BAD_ARG + : _mkdir(dir_path) ? zix_errno_status(errno) + : ZIX_STATUS_SUCCESS; +} + +ZixStatus +zix_create_directory_like(const char* const dir_path, + const char* const existing_path) +{ + return (zix_file_type(existing_path) != ZIX_FILE_TYPE_DIRECTORY) + ? ZIX_STATUS_NOT_FOUND + : zix_create_directory(dir_path); +} + +ZixStatus +zix_create_symlink(const char* const target_path, const char* const link_path) +{ +#if USE_CREATESYMBOLICLINK + static const DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + + return zix_windows_status(CreateSymbolicLink(link_path, target_path, flags)); +#else + return ZIX_STATUS_NOT_SUPPORTED; +#endif +} + +ZixStatus +zix_create_directory_symlink(const char* const target_path, + const char* const link_path) +{ +#if USE_CREATESYMBOLICLINK + static const DWORD flags = + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + + return zix_windows_status(CreateSymbolicLink(link_path, target_path, flags)); +#else + return ZIX_STATUS_NOT_SUPPORTED; +#endif +} + +ZixStatus +zix_create_hard_link(const char* const target_path, const char* const link_path) +{ + return zix_windows_status(CreateHardLink(link_path, target_path, NULL)); +} + +char* +zix_temp_directory_path(ZixAllocator* const allocator) +{ + const DWORD size = GetTempPath(0U, NULL); + char* const buf = (char*)zix_calloc(allocator, size, 1); + if (buf && (GetTempPath(size, buf) != size - 1U)) { + zix_free(allocator, buf); + return NULL; + } + + return buf; +} + +char* +zix_current_path(ZixAllocator* const allocator) +{ + const DWORD size = GetCurrentDirectory(0U, NULL); + char* const buf = (char*)zix_calloc(allocator, size, 1); + if (buf && (GetCurrentDirectory(size, buf) != size - 1U)) { + zix_free(allocator, buf); + return NULL; + } + + return buf; +} diff --git a/src/win32/system_win32.c b/src/win32/system_win32.c new file mode 100644 index 0000000..a2e68bf --- /dev/null +++ b/src/win32/system_win32.c @@ -0,0 +1,20 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "../system.h" + +#include <windows.h> + +#include <limits.h> +#include <stdint.h> + +uint32_t +zix_system_page_size(void) +{ + SYSTEM_INFO info; + GetSystemInfo(&info); + + return (info.dwPageSize > 0 && info.dwPageSize < UINT32_MAX) + ? (uint32_t)info.dwPageSize + : 512U; +} diff --git a/src/zix_config.h b/src/zix_config.h index b1096a5..7b87826 100644 --- a/src/zix_config.h +++ b/src/zix_config.h @@ -48,6 +48,53 @@ # endif # endif +// MacOS: clonefile() +# ifndef HAVE_CLONEFILE +# if defined(__APPLE__) && __has_include(<sys/clonefile.h>) +# define HAVE_CLONEFILE 1 +# else +# define HAVE_CLONEFILE 0 +# endif +# endif + +// FreeBSD 13, Linux 4.5, and glibc 2.27: copy_file_range() +# ifndef HAVE_COPY_FILE_RANGE +# if (defined(__FreeBSD__) && __FreeBSD__ >= 13) || defined(__linux__) || \ + (defined(__GLIBC__) && \ + (__GLIBC__ > 2 || __GLIBC__ == 2 && __GLIBC_MINOR__ >= 27)) +# define HAVE_COPY_FILE_RANGE 1 +# else +# define HAVE_COPY_FILE_RANGE 0 +# endif +# endif + +// Windows: CreateSymbolicLink() +# ifndef HAVE_CREATESYMBOLICLINK +# if defined(_MSC_VER) && _MSC_VER >= 1910 +# define HAVE_CREATESYMBOLICLINK 1 +# else +# define HAVE_CREATESYMBOLICLINK 0 +# endif +# endif + +// POSIX.1-2001, Windows: fileno() +# ifndef HAVE_FILENO +# if defined(_WIN32) || defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_FILENO 1 +# else +# define HAVE_FILENO 0 +# endif +# endif + +// Classic UNIX: flock() +# ifndef HAVE_FLOCK +# if defined(__APPLE__) || defined(__unix__) +# define HAVE_FLOCK 1 +# else +# define HAVE_FLOCK 0 +# endif +# endif + // POSIX.1-2001: mlock() # ifndef HAVE_MLOCK # if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L @@ -57,6 +104,24 @@ # endif # endif +// POSIX.1-2001: pathconf() +# ifndef HAVE_PATHCONF +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_PATHCONF 1 +# else +# define HAVE_PATHCONF 0 +# endif +# endif + +// POSIX.1-2001: posix_fadvise() +# ifndef HAVE_POSIX_FADVISE +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_POSIX_FADVISE 1 +# else +# define HAVE_POSIX_FADVISE 0 +# endif +# endif + // POSIX.1-2001: posix_memalign() # ifndef HAVE_POSIX_MEMALIGN # if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L @@ -66,6 +131,15 @@ # endif # endif +// POSIX.1-2001: realpath() +# ifndef HAVE_REALPATH +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_REALPATH 1 +# else +# define HAVE_REALPATH 0 +# endif +# endif + // POSIX.1-2001: sem_timedwait() # ifndef HAVE_SEM_TIMEDWAIT # if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L @@ -75,6 +149,15 @@ # endif # endif +// POSIX.1-2001: sysconf() +# ifndef HAVE_SYSCONF +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_SYSCONF 1 +# else +# define HAVE_SYSCONF 0 +# endif +# endif + #endif // !defined(ZIX_NO_DEFAULT_CONFIG) /* @@ -91,22 +174,76 @@ # define USE_CLOCK_GETTIME 0 #endif +#if HAVE_CLONEFILE +# define USE_CLONEFILE 1 +#else +# define USE_CLONEFILE 0 +#endif + +#if HAVE_COPY_FILE_RANGE +# define USE_COPY_FILE_RANGE 1 +#else +# define USE_COPY_FILE_RANGE 0 +#endif + +#if HAVE_CREATESYMBOLICLINK +# define USE_CREATESYMBOLICLINK 1 +#else +# define USE_CREATESYMBOLICLINK 0 +#endif + +#if HAVE_FILENO +# define USE_FILENO 1 +#else +# define USE_FILENO 0 +#endif + +#if HAVE_FLOCK +# define USE_FLOCK 1 +#else +# define USE_FLOCK 0 +#endif + #if HAVE_MLOCK # define USE_MLOCK 1 #else # define USE_MLOCK 0 #endif +#if HAVE_PATHCONF +# define USE_PATHCONF 1 +#else +# define USE_PATHCONF 0 +#endif + +#if HAVE_POSIX_FADVISE +# define USE_POSIX_FADVISE 1 +#else +# define USE_POSIX_FADVISE 0 +#endif + #if HAVE_POSIX_MEMALIGN # define USE_POSIX_MEMALIGN 1 #else # define USE_POSIX_MEMALIGN 0 #endif +#if HAVE_REALPATH +# define USE_REALPATH 1 +#else +# define USE_REALPATH 0 +#endif + #if HAVE_SEM_TIMEDWAIT # define USE_SEM_TIMEDWAIT 1 #else # define USE_SEM_TIMEDWAIT 0 #endif +#if HAVE_SYSCONF +# define USE_SYSCONF 1 +#else +# define USE_SYSCONF 0 +#endif + #endif // ZIX_CONFIG_H diff --git a/test/.clang-tidy b/test/.clang-tidy index 5722b69..16da00c 100644 --- a/test/.clang-tidy +++ b/test/.clang-tidy @@ -3,6 +3,7 @@ Checks: > -*-magic-numbers, + -android-cloexec-fopen, -bugprone-easily-swappable-parameters, -cert-err33-c, -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, diff --git a/test/headers/test_headers.c b/test/headers/test_headers.c index 9e5bddd..d62dc2e 100644 --- a/test/headers/test_headers.c +++ b/test/headers/test_headers.c @@ -7,6 +7,7 @@ #include "zix/btree.h" // IWYU pragma: keep #include "zix/bump_allocator.h" // IWYU pragma: keep #include "zix/digest.h" // IWYU pragma: keep +#include "zix/filesystem.h" // IWYU pragma: keep #include "zix/function_types.h" // IWYU pragma: keep #include "zix/hash.h" // IWYU pragma: keep #include "zix/path.h" // IWYU pragma: keep diff --git a/test/test_filesystem.c b/test/test_filesystem.c new file mode 100644 index 0000000..b0ec2a4 --- /dev/null +++ b/test/test_filesystem.c @@ -0,0 +1,712 @@ +// Copyright 2020-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#undef NDEBUG + +#include "zix/allocator.h" +#include "zix/filesystem.h" +#include "zix/path.h" +#include "zix/status.h" +#include "zix/string_view.h" + +#ifndef _WIN32 +# include <unistd.h> +#endif + +#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# include <sys/socket.h> +# include <sys/stat.h> +# include <sys/un.h> +#endif + +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void +test_temp_directory_path(void) +{ + char* tmpdir = zix_temp_directory_path(NULL); + + assert(zix_file_type(tmpdir) == ZIX_FILE_TYPE_DIRECTORY); + + free(tmpdir); +} + +static void +test_current_path(void) +{ + char* cwd = zix_current_path(NULL); + + assert(zix_file_type(cwd) == ZIX_FILE_TYPE_DIRECTORY); + + free(cwd); +} + +static char* +create_temp_dir(const char* const name_pattern) +{ + char* const temp = zix_temp_directory_path(NULL); + char* const path_pattern = zix_path_join(NULL, temp, name_pattern); + char* const result = zix_create_temporary_directory(NULL, path_pattern); + free(path_pattern); + zix_free(NULL, temp); + return result; +} + +static void +test_canonical_path(void) +{ + assert(!zix_canonical_path(NULL, NULL)); + + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + assert(file_path); + + { + FILE* const f = fopen(file_path, "w"); + fprintf(f, "test\n"); + fclose(f); + } + +#ifndef _WIN32 + // Test symlink resolution + + char* const link_path = zix_path_join(NULL, temp_dir, "zix_test_link"); + + assert(!zix_create_symlink(file_path, link_path)); + + char* const real_file_path = zix_canonical_path(NULL, file_path); + char* const real_link_path = zix_canonical_path(NULL, link_path); + + assert(real_file_path); + assert(real_link_path); + assert(!strcmp(real_file_path, real_link_path)); + + assert(!zix_remove(link_path)); + free(real_link_path); + free(real_file_path); + free(link_path); +#endif + + // Test dot segment resolution + + char* const parent_dir_1 = zix_path_join(NULL, temp_dir, ".."); + assert(parent_dir_1); + + const ZixStringView parent_view = zix_path_parent_path(temp_dir); + char* const parent_dir_2 = zix_string_view_copy(NULL, parent_view); + assert(parent_dir_2); + assert(parent_dir_2[0]); + + char* const real_parent_dir_1 = zix_canonical_path(NULL, parent_dir_1); + char* const real_parent_dir_2 = zix_canonical_path(NULL, parent_dir_2); + + assert(real_parent_dir_1); + assert(real_parent_dir_2); + assert(!strcmp(real_parent_dir_1, real_parent_dir_2)); + + // Test missing files + + assert(!zix_canonical_path(NULL, "/presumuably/absent")); + assert(!zix_canonical_path(NULL, "/presumuably/absent/")); + assert(!zix_canonical_path(NULL, "presumuably_absent")); + + // Clean everything up + + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + + free(real_parent_dir_2); + free(real_parent_dir_1); + free(parent_dir_2); + free(parent_dir_1); + free(file_path); + free(temp_dir); +} + +static void +test_file_type(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + assert(file_path); + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE); + + // Regular file + FILE* f = fopen(file_path, "w"); + fprintf(f, "test\n"); + fclose(f); + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR); + assert(!zix_remove(file_path)); + +#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L + + // FIFO + if (!mkfifo(file_path, 0600)) { + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_FIFO); + assert(!zix_remove(file_path)); + } + + // Socket + const int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock >= 0) { + const socklen_t addr_len = sizeof(struct sockaddr_un); + struct sockaddr_un* const addr = (struct sockaddr_un*)calloc(1, addr_len); + + addr->sun_family = AF_UNIX; + strncpy(addr->sun_path, file_path, sizeof(addr->sun_path) - 1); + + const int fd = bind(sock, (struct sockaddr*)addr, addr_len); + if (fd >= 0) { + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_SOCKET); + assert(!zix_remove(file_path)); + } + close(fd); + close(sock); + free(addr); + } + +#endif + + assert(!zix_remove(temp_dir)); + free(file_path); + free(temp_dir); +} + +static void +test_path_exists(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + assert(temp_dir); + assert(file_path); + + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE); + + FILE* f = fopen(file_path, "w"); + fprintf(f, "test\n"); + fclose(f); + + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR); + + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + + free(file_path); + free(temp_dir); +} + +static void +test_is_directory(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + assert(temp_dir); + assert(file_path); + + assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY); + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE); + + FILE* f = fopen(file_path, "w"); + fprintf(f, "test\n"); + fclose(f); + + assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR); + + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + + free(file_path); + free(temp_dir); +} + +static int +write_to_path(const char* const path, const char* const contents) +{ + FILE* const f = fopen(path, "w"); + if (!f) { + return -1; + } + + const size_t len = strlen(contents); + fwrite(contents, 1, len, f); + + const int ret = fflush(f) ? errno : ferror(f) ? EBADF : 0; + + return fclose(f) ? errno : ret; +} + +static void +test_copy_file(const char* const data_file_path) +{ + ZixStatus st = ZIX_STATUS_SUCCESS; + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const tmp_file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + char* const copy_path = zix_path_join(NULL, temp_dir, "zix_test_copy"); + assert(temp_dir); + assert(tmp_file_path); + assert(copy_path); + + write_to_path(tmp_file_path, "test\n"); + + assert((st = zix_copy_file(NULL, tmp_file_path, "/does/not/exist", 0U))); + assert(zix_copy_file(NULL, "/does/not/exist", copy_path, 0U)); + assert(zix_file_type(copy_path) == ZIX_FILE_TYPE_NONE); + + // Fail to copy from/to a directory + assert(zix_copy_file(NULL, temp_dir, copy_path, 0U)); + assert(zix_copy_file(NULL, tmp_file_path, temp_dir, 0U)); + + if (data_file_path) { + // Fail to copy a file to itself + assert((st = zix_copy_file(NULL, data_file_path, data_file_path, 0U)) == + ZIX_STATUS_EXISTS); + + // Successful copy between filesystems + assert(!(st = zix_copy_file(NULL, data_file_path, copy_path, 0U))); + assert(zix_file_equals(NULL, data_file_path, copy_path)); + + // Trying the same again fails because the copy path already exists + assert(zix_copy_file(NULL, data_file_path, copy_path, 0U) == + ZIX_STATUS_EXISTS); + + // Unless overwriting is requested + assert(!zix_copy_file( + NULL, data_file_path, copy_path, ZIX_COPY_OPTION_OVERWRITE_EXISTING)); + + assert(!zix_remove(copy_path)); + } + + // Successful copy within a filesystem + assert(zix_file_type(copy_path) == ZIX_FILE_TYPE_NONE); + assert(!(st = zix_copy_file(NULL, tmp_file_path, copy_path, 0U))); + assert(zix_file_equals(NULL, tmp_file_path, copy_path)); + assert(!zix_remove(copy_path)); + + if (zix_file_type("/dev/random") == ZIX_FILE_TYPE_CHARACTER) { + // Fail to copy infinite file to a file + assert((st = zix_copy_file(NULL, "/dev/random", copy_path, 0U)) == + ZIX_STATUS_BAD_ARG); + + // Fail to copy infinite file to itself + assert((st = zix_copy_file(NULL, "/dev/random", "/dev/random", 0U)) == + ZIX_STATUS_BAD_ARG); + + // Fail to copy infinite file to another + assert((st = zix_copy_file(NULL, "/dev/random", "/dev/urandom", 0U)) == + ZIX_STATUS_BAD_ARG); + } + + if (zix_file_type("/dev/full") == ZIX_FILE_TYPE_CHARACTER) { + if (data_file_path) { + assert((st = zix_copy_file(NULL, + data_file_path, + "/dev/full", + ZIX_COPY_OPTION_OVERWRITE_EXISTING)) == + ZIX_STATUS_NO_SPACE); + } + + // Copy short file (error after flushing) + assert(( + st = zix_copy_file( + NULL, tmp_file_path, "/dev/full", ZIX_COPY_OPTION_OVERWRITE_EXISTING))); + + // Copy long file (error during writing) + FILE* const f = fopen(tmp_file_path, "w"); + for (size_t i = 0; i < 4096; ++i) { + fprintf(f, "test\n"); + } + fclose(f); + assert(( + st = zix_copy_file( + NULL, tmp_file_path, "/dev/full", ZIX_COPY_OPTION_OVERWRITE_EXISTING))); + } + + assert(!(st = zix_remove(tmp_file_path))); + assert(!(st = zix_remove(temp_dir))); + + free(copy_path); + free(tmp_file_path); + free(temp_dir); +} + +static void +test_flock(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + assert(temp_dir); + assert(file_path); + + FILE* const f1 = fopen(file_path, "w"); + FILE* const f2 = fopen(file_path, "w"); + + ZixStatus st = zix_file_lock(f1, ZIX_FILE_LOCK_TRY); + assert(!st || st == ZIX_STATUS_NOT_SUPPORTED); + + if (!st) { + assert(zix_file_lock(f2, ZIX_FILE_LOCK_TRY) == ZIX_STATUS_UNAVAILABLE); + assert(!zix_file_unlock(f1, ZIX_FILE_LOCK_TRY)); + + // assert(zix_file_unlock(f1, ZIX_FILE_LOCK_TRY)); + // assert(zix_file_unlock(f1, ZIX_FILE_LOCK_BLOCK)); + } + + // assert(zix_file_unlock(stdout, ZIX_FILE_LOCK_TRY)); + // assert(zix_file_unlock(stdout, ZIX_FILE_LOCK_BLOCK)); + + fclose(f2); + fclose(f1); + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + free(file_path); + free(temp_dir); +} + +typedef struct { + size_t n_names; + char** names; +} FileList; + +static void +visit(const char* const path, const char* const name, void* const data) +{ + (void)path; + + const size_t name_len = strlen(name); + FileList* const file_list = (FileList*)data; + + char** const new_names = + (char**)realloc(file_list->names, sizeof(char*) * ++file_list->n_names); + + if (new_names) { + char* const name_copy = (char*)calloc(name_len + 1, 1); + memcpy(name_copy, name, name_len + 1); + + file_list->names = new_names; + file_list->names[file_list->n_names - 1] = name_copy; + } +} + +static void +test_dir_for_each(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const path1 = zix_path_join(NULL, temp_dir, "zix_test_1"); + char* const path2 = zix_path_join(NULL, temp_dir, "zix_test_2"); + assert(temp_dir); + assert(path1); + assert(path2); + + FILE* const f1 = fopen(path1, "w"); + FILE* const f2 = fopen(path2, "w"); + fprintf(f1, "test\n"); + fprintf(f2, "test\n"); + fclose(f2); + fclose(f1); + + FileList file_list = {0, NULL}; + zix_dir_for_each(temp_dir, &file_list, visit); + + assert(file_list.names); + assert((!strcmp(file_list.names[0], "zix_test_1") && + !strcmp(file_list.names[1], "zix_test_2")) || + (!strcmp(file_list.names[0], "zix_test_2") && + !strcmp(file_list.names[1], "zix_test_1"))); + + assert(!zix_remove(path2)); + assert(!zix_remove(path1)); + assert(!zix_remove(temp_dir)); + + free(file_list.names[0]); + free(file_list.names[1]); + free(file_list.names); + free(path2); + free(path1); + free(temp_dir); +} + +static void +test_create_temporary_directory(void) +{ + assert(!zix_create_temporary_directory(NULL, "")); + + char* const path1 = create_temp_dir("zixXXXXXX"); + + assert(path1); + assert(zix_file_type(path1) == ZIX_FILE_TYPE_DIRECTORY); + + char* const path2 = create_temp_dir("zixXXXXXX"); + + assert(path2); + assert(strcmp(path1, path2)); + assert(zix_file_type(path1) == ZIX_FILE_TYPE_DIRECTORY); + assert(zix_file_type(path2) == ZIX_FILE_TYPE_DIRECTORY); + + assert(!zix_remove(path2)); + assert(!zix_remove(path1)); + free(path2); + free(path1); +} + +static void +test_create_directory_like(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY); + + char* const sub_dir = zix_path_join(NULL, temp_dir, "sub"); + assert(zix_create_directory_like(sub_dir, sub_dir) == ZIX_STATUS_NOT_FOUND); + assert(!zix_create_directory_like(sub_dir, temp_dir)); + assert(zix_file_type(sub_dir) == ZIX_FILE_TYPE_DIRECTORY); + assert(!zix_remove(sub_dir)); + zix_free(NULL, sub_dir); + + assert(!zix_remove(temp_dir)); + free(temp_dir); +} + +static void +test_create_directories(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + + assert(temp_dir); + assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY); + assert(zix_create_directories(NULL, "") == ZIX_STATUS_BAD_ARG); + + char* const child_dir = zix_path_join(NULL, temp_dir, "child"); + char* const grandchild_dir = zix_path_join(NULL, child_dir, "grandchild"); + + assert(!zix_create_directories(NULL, grandchild_dir)); + assert(zix_file_type(grandchild_dir) == ZIX_FILE_TYPE_DIRECTORY); + assert(zix_file_type(child_dir) == ZIX_FILE_TYPE_DIRECTORY); + + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + FILE* const f = fopen(file_path, "w"); + + fprintf(f, "test\n"); + fclose(f); + + assert(zix_create_directories(NULL, file_path) == ZIX_STATUS_EXISTS); + + assert(!zix_remove(file_path)); + assert(!zix_remove(grandchild_dir)); + assert(!zix_remove(child_dir)); + assert(!zix_remove(temp_dir)); + free(file_path); + free(child_dir); + free(grandchild_dir); + free(temp_dir); +} + +static void +test_file_equals(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const path1 = zix_path_join(NULL, temp_dir, "zix1"); + char* const path2 = zix_path_join(NULL, temp_dir, "zix2"); + assert(temp_dir); + + // Equal: test, test + assert(!write_to_path(path1, "test")); + assert(!write_to_path(path2, "test")); + assert(zix_file_equals(NULL, path1, path2)); + + // Missing files + assert(!zix_file_equals(NULL, path1, "/does/not/exist")); + assert(!zix_file_equals(NULL, "/does/not/exist", path2)); + + // Longer RHS: test, testdiff + assert(!write_to_path(path2, "diff")); + assert(!zix_file_equals(NULL, path1, path2)); + + // Longer LHS: testdifflong, testdiff + assert(!write_to_path(path1, "difflong")); + assert(!zix_file_equals(NULL, path1, path2)); + + // Equal sizes but different content: testdifflong, testdifflang + assert(!write_to_path(path2, "difflang")); + assert(!zix_file_equals(NULL, path1, path2)); + + assert(zix_file_equals(NULL, path1, path1)); + assert(zix_file_equals(NULL, path2, path2)); + + assert(!zix_remove(path2)); + assert(!zix_remove(path1)); + assert(!zix_remove(temp_dir)); + + free(path2); + free(path1); + free(temp_dir); +} + +static void +test_file_size(void) +{ + static const char* const contents = "file size test"; + + char* const temp_dir = create_temp_dir("zixXXXXXX"); + char* const path = zix_path_join(NULL, temp_dir, "zix_test"); + + assert(temp_dir); + assert(!write_to_path(path, contents)); + + const ZixFileOffset size = zix_file_size(path); + assert(size > 0); + assert((size_t)size == strlen(contents)); + + assert(!zix_remove(path)); + assert(!zix_remove(temp_dir)); + + free(path); + free(temp_dir); +} + +static void +test_create_symlink(void) +{ + static const char* const contents = "zixtest"; + + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + + // Write contents to original file + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + { + FILE* const f = fopen(file_path, "w"); + fprintf(f, "%s", contents); + fclose(f); + } + + // Ensure original file exists and is a regular file + assert(zix_symlink_type(file_path) == ZIX_FILE_TYPE_REGULAR); + + // Create symlink to original file + char* const link_path = zix_path_join(NULL, temp_dir, "zix_test_link"); + const ZixStatus st = zix_create_symlink(file_path, link_path); + + // Check that the symlink seems equivalent to the original file + assert(!st || st == ZIX_STATUS_NOT_SUPPORTED || st == ZIX_STATUS_BAD_PERMS); + if (!st) { + assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_SYMLINK); + assert(zix_file_type(link_path) == ZIX_FILE_TYPE_REGULAR); + assert(zix_file_equals(NULL, file_path, link_path)); + assert(!zix_remove(link_path)); + } else { + assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_NONE); + } + + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + + free(link_path); + free(file_path); + free(temp_dir); +} + +static void +test_create_directory_symlink(void) +{ + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + + char* const link_path = zix_path_join(NULL, temp_dir, "zix_test_link"); + const ZixStatus st = zix_create_directory_symlink(temp_dir, link_path); + + if (st != ZIX_STATUS_NOT_SUPPORTED && st != ZIX_STATUS_BAD_PERMS) { + assert(!st); + assert(zix_file_type(link_path) == ZIX_FILE_TYPE_DIRECTORY); + +#ifdef _WIN32 + assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_DIRECTORY); +#else + assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_SYMLINK); +#endif + + assert(!zix_remove(link_path)); + } + + assert(!zix_remove(temp_dir)); + free(link_path); + free(temp_dir); +} + +static void +test_create_hard_link(void) +{ + static const char* const contents = "zixtest"; + + char* const temp_dir = create_temp_dir("zixXXXXXX"); + assert(temp_dir); + + // Write contents to original file + char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file"); + { + FILE* const f = fopen(file_path, "w"); + fprintf(f, "%s", contents); + fclose(f); + } + + // Ensure original file exists and is a regular file + assert(zix_symlink_type(file_path) == ZIX_FILE_TYPE_REGULAR); + + // Create symlink to original file + char* const link_path = zix_path_join(NULL, temp_dir, "zix_test_link"); + const ZixStatus st = zix_create_hard_link(file_path, link_path); + + // Check that the link is equivalent to the original file + assert(!st || st == ZIX_STATUS_NOT_SUPPORTED || st == ZIX_STATUS_MAX_LINKS); + if (!st) { + assert(zix_file_type(link_path) == ZIX_FILE_TYPE_REGULAR); + assert(zix_file_equals(NULL, file_path, link_path)); + assert(!zix_remove(link_path)); + } else { + assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_NONE); + } + + assert(!zix_remove(file_path)); + assert(!zix_remove(temp_dir)); + + free(link_path); + free(file_path); + free(temp_dir); +} + +int +main(const int argc, char** const argv) +{ +#ifdef __EMSCRIPTEN__ + const char* const data_file_path = NULL; +#else + const char* const data_file_path = argc > 1 ? argv[1] : "build.ninja"; +#endif + + test_temp_directory_path(); + test_current_path(); + test_canonical_path(); + test_file_type(); + test_path_exists(); + test_is_directory(); + test_copy_file(data_file_path); + test_flock(); + test_dir_for_each(); + test_create_temporary_directory(); + test_create_directory_like(); + test_create_directories(); + test_file_equals(); + test_file_size(); + test_create_symlink(); + test_create_directory_symlink(); + test_create_hard_link(); + + return 0; +} diff --git a/test/test_status.c b/test/test_status.c index ca47d6e..6857928 100644 --- a/test/test_status.c +++ b/test/test_status.c @@ -1,4 +1,4 @@ -// Copyright 2021 David Robillard <d@drobilla.net> +// Copyright 2021-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #undef NDEBUG @@ -15,7 +15,7 @@ test_strerror(void) const char* msg = zix_strerror(ZIX_STATUS_SUCCESS); assert(!strcmp(msg, "Success")); - for (int i = ZIX_STATUS_ERROR; i <= ZIX_STATUS_UNAVAILABLE; ++i) { + for (int i = ZIX_STATUS_ERROR; i <= ZIX_STATUS_MAX_LINKS; ++i) { msg = zix_strerror((ZixStatus)i); assert(strcmp(msg, "Success")); } |