summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--include/zix/filesystem.h371
-rw-r--r--include/zix/path.h3
-rw-r--r--include/zix/status.h2
-rw-r--r--include/zix/zix.h1
-rw-r--r--meson.build119
-rw-r--r--meson/suppressions/meson.build2
-rw-r--r--src/errno_status.c5
-rw-r--r--src/filesystem.c122
-rw-r--r--src/posix/filesystem_posix.c444
-rw-r--r--src/posix/system_posix.c26
-rw-r--r--src/status.c6
-rw-r--r--src/system.c41
-rw-r--r--src/system.h30
-rw-r--r--src/win32/filesystem_win32.c325
-rw-r--r--src/win32/system_win32.c20
-rw-r--r--src/zix_config.h137
-rw-r--r--test/.clang-tidy1
-rw-r--r--test/headers/test_headers.c1
-rw-r--r--test/test_filesystem.c712
-rw-r--r--test/test_status.c4
21 files changed, 2368 insertions, 5 deletions
diff --git a/README.md b/README.md
index 9a9454f..46122e3 100644
--- a/README.md
+++ b/README.md
@@ -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"));
}