summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2022-10-23 13:41:15 -0400
committerDavid Robillard <d@drobilla.net>2022-10-23 13:46:58 -0400
commitc886d489576cd0bc33d7d22d81981c794067946f (patch)
treef43c8d872401ed80b37f974516d9e9f2fee9d765
parentf95a698b94069ed03f6a8f2d0f7eb089d66c91ef (diff)
downloadzix-c886d489576cd0bc33d7d22d81981c794067946f.tar.gz
zix-c886d489576cd0bc33d7d22d81981c794067946f.tar.bz2
zix-c886d489576cd0bc33d7d22d81981c794067946f.zip
Add path API
-rw-r--r--.clang-format1
-rw-r--r--.gitlab-ci.yml16
-rw-r--r--README.md4
-rw-r--r--include/zix/function_types.h2
-rw-r--r--include/zix/path.h246
-rw-r--r--include/zix/zix.h8
-rw-r--r--meson.build20
-rw-r--r--meson_options.txt3
-rw-r--r--src/index_range.h28
-rw-r--r--src/path.c712
-rw-r--r--src/path_iter.h31
-rw-r--r--test/cpp/.clang-tidy13
-rw-r--r--test/cpp/test_path_std.cpp496
-rw-r--r--test/headers/test_headers.c1
-rw-r--r--test/test_path.c948
15 files changed, 2523 insertions, 6 deletions
diff --git a/.clang-format b/.clang-format
index d5bb2b7..e8e993c 100644
--- a/.clang-format
+++ b/.clang-format
@@ -26,6 +26,7 @@ StatementMacros:
- ZIX_END_DECLS
- ZIX_MALLOC_API
- ZIX_PURE_API
+ - ZIX_PURE_WIN_API
- ZIX_THREAD_FUNC
- ZIX_UNUSED
- _Pragma
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c03bd21..1c7d73e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -55,13 +55,13 @@ x64_static:
x64_sanitize:
image: lv2plugin/debian-x64-clang
script:
- - meson setup build -Db_lundef=false -Dbuildtype=plain -Dstrict=true -Dwerror=true -Dc_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dc_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability"
- - ninja -C build test
- - meson configure build -Dbuildtype=debugoptimized -Dc_args="" -Dc_link_args=""
- - meson configure build -Db_sanitize=memory
+ - meson setup build -Db_lundef=false -Dbuildtype=plain -Dstrict=true -Dwerror=true -Dc_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dc_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dcpp_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" -Dcpp_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero"
- ninja -C build test
+ - meson configure build -Dbuildtype=debugoptimized -Dc_args="" -Dc_link_args="" -Dcpp_args="" -Dcpp_link_args=""
- meson configure build -Db_sanitize=thread
- ninja -C build test
+ - meson configure build -Db_sanitize=memory -Dtests_cpp=disabled
+ - ninja -C build test
variables:
CC: "clang"
@@ -93,12 +93,16 @@ mingw32_dbg:
script:
- meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
- ninja -C build test
+ variables:
+ WINEPATH: "Z:\\usr\\lib\\gcc\\i686-w64-mingw32\\10-win32"
mingw32_rel:
image: lv2plugin/debian-mingw32
script:
- meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
- ninja -C build test
+ variables:
+ WINEPATH: "Z:\\usr\\lib\\gcc\\i686-w64-mingw32\\10-win32"
mingw64_dbg:
@@ -106,12 +110,16 @@ mingw64_dbg:
script:
- meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
- ninja -C build test
+ variables:
+ WINEPATH: "Z:\\usr\\lib\\gcc\\x86_64-w64-mingw32\\10-win32"
mingw64_rel:
image: lv2plugin/debian-mingw64
script:
- meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
- ninja -C build test
+ variables:
+ WINEPATH: "Z:\\usr\\lib\\gcc\\x86_64-w64-mingw32\\10-win32"
mac_dbg:
diff --git a/README.md b/README.md
index 1baef6f..9a9454f 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,9 @@ Components
* `ZixSem`: A portable semaphore wrapper.
* `ZixThread`: A portable thread wrapper.
* `ZixTree`: A binary search tree.
- * `zix_digest`: A hash function for arbitrary data.
+
+ * `zix/digest.h`: Digest functions suitable for hashing arbitrary data.
+ * `zix/path.h`: Functions for working with filesystem paths lexically.
Platforms
---------
diff --git a/include/zix/function_types.h b/include/zix/function_types.h
index e2a8958..d0c7193 100644
--- a/include/zix/function_types.h
+++ b/include/zix/function_types.h
@@ -12,7 +12,7 @@ ZIX_BEGIN_DECLS
/**
@defgroup zix_function_types Function Types
- @ingroup zix
+ @ingroup zix_utilities
@{
*/
diff --git a/include/zix/path.h b/include/zix/path.h
new file mode 100644
index 0000000..5d3bd60
--- /dev/null
+++ b/include/zix/path.h
@@ -0,0 +1,246 @@
+// Copyright 2007-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef ZIX_PATH_H
+#define ZIX_PATH_H
+
+#include "zix/allocator.h"
+#include "zix/attributes.h"
+#include "zix/string_view.h"
+
+#include <stdbool.h>
+
+/// A pure API function on Windows, a constant stub everywhere else
+#ifdef _WIN32
+# define ZIX_PURE_WIN_API ZIX_PURE_API
+#else
+# define ZIX_PURE_WIN_API ZIX_CONST_API
+#endif
+
+ZIX_BEGIN_DECLS
+
+/**
+ @defgroup zix_path Path
+ @ingroup zix_file_system
+
+ Functions for interpreting and manipulating paths. These functions are
+ purely lexical and do not access any filesystem.
+
+ @{
+*/
+
+/**
+ @defgroup zix_path_concatenation Concatenation
+ @{
+*/
+
+/// Join path `a` and path `b` with a single directory separator between them
+ZIX_API
+char* ZIX_ALLOCATED
+zix_path_join(ZixAllocator* ZIX_NULLABLE allocator,
+ const char* ZIX_NULLABLE a,
+ const char* ZIX_NULLABLE b);
+
+/**
+ @}
+ @defgroup zix_path_lexical Lexical Transformations
+ @{
+*/
+
+/**
+ Return `path` with preferred directory separators.
+
+ The returned path will be a copy of `path` with any directory separators
+ converted to the preferred separator (backslash on Windows, slash everywhere
+ else).
+*/
+ZIX_API
+char* ZIX_ALLOCATED
+zix_path_preferred(ZixAllocator* ZIX_NULLABLE allocator,
+ const char* ZIX_NONNULL path);
+
+/**
+ Return `path` converted to normal form.
+
+ Paths in normal form have all dot segments removed and use only a single
+ preferred separator for all separators (that is, any number of separators is
+ 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.
+*/
+ZIX_API
+char* ZIX_ALLOCATED
+zix_path_lexically_normal(ZixAllocator* ZIX_NULLABLE allocator,
+ const char* ZIX_NONNULL path);
+
+/**
+ Return `path` relative to `base` if possible.
+
+ If `path` is not within `base`, a copy is returned. Otherwise, an
+ equivalent path relative to `base` is returned (which may contain
+ up-references).
+*/
+ZIX_API
+char* ZIX_ALLOCATED
+zix_path_lexically_relative(ZixAllocator* ZIX_NULLABLE allocator,
+ const char* ZIX_NONNULL path,
+ const char* ZIX_NONNULL base);
+
+/**
+ @}
+ @defgroup zix_path_decomposition Decomposition
+ @{
+*/
+
+/// Return the root name of `path` like "C:", or null
+ZIX_PURE_WIN_API
+ZixStringView
+zix_path_root_name(const char* ZIX_NONNULL path);
+
+/// Return the root directory of `path` like "/" or "\", or null
+ZIX_PURE_API
+ZixStringView
+zix_path_root_directory(const char* ZIX_NONNULL path);
+
+/**
+ Return the root path of `path`, or null.
+
+ The universal root path (in normal form) is "/". Root paths are their own
+ roots, but note that the path returned by this function may be partially
+ normalized. For example, "/" is the root of "/", "//", "/.", and "/..".
+
+ On Windows, the root may additionally be an absolute drive root like "C:\",
+ a relative drive root like "C:", or a network root like "//Host/".
+
+ @return The newly allocated root path of `path`, or null if it has no root
+ or allocation failed.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_root_path(const char* ZIX_NONNULL path);
+
+/**
+ Return the relative path component of path without the root directory.
+
+ If the path has no relative path (because it is empty or a root path), this
+ returns null.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_relative_path(const char* ZIX_NONNULL path);
+
+/**
+ Return the path to the directory that contains `path`.
+
+ The parent of a root path is itself, but note that the path returned by this
+ function may be partially normalized. For example, "/" is the parent of
+ "/", "//", "/.", and "/..".
+
+ If `path` has a trailing separator, it is treated like an empty filename in
+ a directory. For example, the parent of "/a/" is "/a".
+
+ If `path` is relative, then this returns either a relative path to the
+ parent if possible, or null. For example, the parent of "a/b" is "a".
+
+ @return The newly allocated path to the parent of `path`, or null if it has
+ no parent or allocation failed.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_parent_path(const char* ZIX_NONNULL path);
+
+/**
+ Return the filename component of `path` without any directories.
+
+ The filename is the name after the last directory separator. If the path
+ has no filename, this returns null.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_filename(const char* ZIX_NONNULL path);
+
+/**
+ Return the stem of the filename component of `path`.
+
+ The "stem" is the filename without the extension, that is, everything up to
+ the last "." if "." is not the first character.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_stem(const char* ZIX_NONNULL path);
+
+/**
+ Return the extension of the filename component of `path`.
+
+ The "extension" is everything past the last "." in the filename, if "." is
+ not the first character.
+*/
+ZIX_PURE_API
+ZixStringView
+zix_path_extension(const char* ZIX_NONNULL path);
+
+/**
+ @}
+ @defgroup zix_path_queries Queries
+ @{
+*/
+
+/// Return true if `path` has a root path like "/" or "C:\"
+ZIX_PURE_API
+bool
+zix_path_has_root_path(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a root name like "C:"
+ZIX_PURE_WIN_API
+bool
+zix_path_has_root_name(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a root directory like "/" or "\"
+ZIX_PURE_API
+bool
+zix_path_has_root_directory(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a relative path "dir/file.txt"
+ZIX_PURE_API
+bool
+zix_path_has_relative_path(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a parent path like "dir/"
+ZIX_PURE_API
+bool
+zix_path_has_parent_path(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a filename like "file.txt"
+ZIX_PURE_API
+bool
+zix_path_has_filename(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has a stem like "file"
+ZIX_PURE_API
+bool
+zix_path_has_stem(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` has an extension like ".txt"
+ZIX_PURE_API
+bool
+zix_path_has_extension(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` is an absolute path
+ZIX_PURE_API
+bool
+zix_path_is_absolute(const char* ZIX_NULLABLE path);
+
+/// Return true if `path` is a relative path
+ZIX_PURE_API
+bool
+zix_path_is_relative(const char* ZIX_NULLABLE path);
+
+/**
+ @}
+ @}
+*/
+
+ZIX_END_DECLS
+
+#endif /* ZIX_PATH_H */
diff --git a/include/zix/zix.h b/include/zix/zix.h
index 223d1dc..a91b6f8 100644
--- a/include/zix/zix.h
+++ b/include/zix/zix.h
@@ -54,6 +54,14 @@
/**
@}
+ @defgroup zix_file_system File System
+ @{
+*/
+
+#include "zix/path.h"
+
+/**
+ @}
@}
*/
diff --git a/meson.build b/meson.build
index 9a64ba6..bf082b9 100644
--- a/meson.build
+++ b/meson.build
@@ -9,6 +9,7 @@ project('zix', ['c'],
'b_ndebug=if-release',
'buildtype=release',
'c_std=c99',
+ 'cpp_std=c++17',
])
major_version = meson.project_version().split('.')[0]
@@ -100,6 +101,7 @@ c_headers = files(
'include/zix/digest.h',
'include/zix/function_types.h',
'include/zix/hash.h',
+ 'include/zix/path.h',
'include/zix/ring.h',
'include/zix/sem.h',
'include/zix/status.h',
@@ -117,6 +119,7 @@ sources = files(
'src/digest.c',
'src/errno_status.c',
'src/hash.c',
+ 'src/path.c',
'src/ring.c',
'src/status.c',
'src/string_view.c',
@@ -227,6 +230,7 @@ sequential_tests = [
'test_btree',
'test_digest',
'test_hash',
+ 'test_path',
'test_status',
'test_tree',
]
@@ -328,6 +332,22 @@ if not get_option('tests').disabled()
),
)
endif
+
+ if add_languages(['cpp'], native: false, required: get_option('tests_cpp'))
+ cpp = meson.get_compiler('cpp')
+
+ test(
+ 'test_path_std',
+ executable(
+ 'test_path_std',
+ files('test/cpp/test_path_std.cpp'),
+ cpp_args: program_c_args,
+ dependencies: [zix_dep],
+ include_directories: include_dirs,
+ link_args: program_link_args,
+ ),
+ )
+ endif
endif
##############
diff --git a/meson_options.txt b/meson_options.txt
index 2893d42..bc25d2f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -13,5 +13,8 @@ option('strict', type: 'boolean', value: false, yield: true,
option('tests', type: 'feature', value: 'auto', yield: true,
description: 'Build tests')
+option('tests_cpp', type: 'feature', value: 'auto', yield: true,
+ description: 'Build C++ standard library comparison tests')
+
option('title', type: 'string', value: 'Zix',
description: 'Project title')
diff --git a/src/index_range.h b/src/index_range.h
new file mode 100644
index 0000000..1e06e90
--- /dev/null
+++ b/src/index_range.h
@@ -0,0 +1,28 @@
+// Copyright 2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef ZIX_INDEX_RANGE_H
+#define ZIX_INDEX_RANGE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef struct {
+ size_t begin; ///< Index to the first character
+ size_t end; ///< Index one past the last character
+} ZixIndexRange;
+
+static inline ZixIndexRange
+zix_make_range(const size_t begin, const size_t end)
+{
+ const ZixIndexRange result = {begin, end};
+ return result;
+}
+
+static inline bool
+zix_is_empty_range(const ZixIndexRange range)
+{
+ return range.begin == range.end;
+}
+
+#endif // ZIX_INDEX_RANGE_H
diff --git a/src/path.c b/src/path.c
new file mode 100644
index 0000000..aa787b3
--- /dev/null
+++ b/src/path.c
@@ -0,0 +1,712 @@
+// Copyright 2007-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#include "zix/path.h"
+
+#include "index_range.h"
+#include "path_iter.h"
+
+#include "zix/string_view.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+static const ZixIndexRange two_range = {0U, 2U};
+static const ZixIndexRange one_range = {0U, 1U};
+
+#ifdef _WIN32
+
+# define ZIX_DIR_SEP '\\' // Backslash is preferred on Windows
+
+static inline bool
+is_dir_sep(const char c)
+{
+ return c == '/' || c == '\\';
+}
+
+static inline bool
+is_any_sep(const char c)
+{
+ return c == '/' || c == ':' || c == '\\';
+}
+
+static bool
+is_root_name_char(const char c)
+{
+ return c && !is_dir_sep(c);
+}
+
+static ZixIndexRange
+zix_path_root_name_range(const char* const path)
+{
+ ZixIndexRange result = {0U, 0U};
+
+ if (path) {
+ if (((path[0] >= 'A' && path[0] <= 'Z') ||
+ (path[0] >= 'a' && path[0] <= 'z')) &&
+ path[1] == ':') {
+ // A root that starts with a letter then ':' has a name like "C:"
+ result.end = 2U;
+
+ } else if (is_dir_sep(path[0]) && is_dir_sep(path[1]) &&
+ is_root_name_char(path[2])) {
+ // A root that starts with two separators has a name like "\\host"
+ result.end = 2U;
+ while (is_root_name_char(path[result.end])) {
+ ++result.end;
+ }
+ }
+ }
+
+ return result;
+}
+
+#else
+
+# define ZIX_DIR_SEP '/' // Slash is the only separator on other platforms
+
+static inline bool
+is_dir_sep(const char c)
+{
+ return c == '/';
+}
+
+static inline bool
+is_any_sep(const char c)
+{
+ return c == '/';
+}
+
+static inline ZixIndexRange
+zix_path_root_name_range(const char* const path)
+{
+ (void)path;
+ return zix_make_range(0U, 0U);
+}
+
+#endif
+
+typedef struct {
+ ZixIndexRange name;
+ ZixIndexRange dir;
+} ZixRootSlices;
+
+static ZixRootSlices
+zix_path_root_slices(const char* const path)
+{
+ // A root name not trailed by a separator has no root directory
+ const ZixIndexRange name = zix_path_root_name_range(path);
+ const size_t dir_len = (size_t)(path && is_dir_sep(path[name.end]));
+ if (!dir_len) {
+ const ZixRootSlices result = {name, {name.end, name.end}};
+ return result;
+ }
+
+ // Skip redundant root directory separators
+ ZixIndexRange dir = {name.end, name.end + dir_len};
+ while (is_dir_sep(path[dir.end])) {
+ dir.begin = dir.end++;
+ }
+
+ const ZixRootSlices result = {name, dir};
+ return result;
+}
+
+static bool
+zix_string_ranges_equal(const char* const lhs,
+ const ZixIndexRange lhs_range,
+ const char* const rhs,
+ const ZixIndexRange rhs_range)
+{
+ const size_t lhs_len = lhs_range.end - lhs_range.begin;
+ const size_t rhs_len = rhs_range.end - rhs_range.begin;
+
+ return lhs_len == rhs_len &&
+ (lhs_len == 0 ||
+ !strncmp(lhs + lhs_range.begin, rhs + rhs_range.begin, lhs_len));
+}
+
+static ZixIndexRange
+zix_path_root_path_range(const char* const path)
+{
+ const ZixRootSlices root = zix_path_root_slices(path);
+ const size_t dir_len = (size_t)!zix_is_empty_range(root.dir);
+
+ return zix_is_empty_range(root.name)
+ ? root.dir
+ : zix_make_range(root.name.begin, root.name.end + dir_len);
+}
+
+static ZixIndexRange
+zix_path_parent_path_range(const ZixStringView path)
+{
+ if (!path.length) {
+ return zix_make_range(0U, 0U); // Empty paths have no parent
+ }
+
+ // Find the first relevant character (skip leading root path if any)
+ const ZixIndexRange root = zix_path_root_path_range(path.data);
+ const size_t p = root.begin;
+
+ // Scan backwards starting at the last character
+ size_t l = path.length - 1U;
+ if (is_dir_sep(path.data[l])) {
+ // Rewind to the last relevant separator (skip trailing redundant ones)
+ while (l > p && is_dir_sep(path.data[l - 1U])) {
+ --l;
+ }
+ } else {
+ // Rewind to the last relevant character (skip trailing name)
+ while (l > p && !is_dir_sep(path.data[l])) {
+ --l;
+ }
+ }
+
+ if (l <= root.end) {
+ return root;
+ }
+
+ // Drop trailing separators
+ while (l > p && is_dir_sep(path.data[l])) {
+ --l;
+ }
+
+ return zix_make_range(root.begin, root.begin + l + 1U - p);
+}
+
+static ZixIndexRange
+zix_path_filename_range(const ZixStringView path)
+{
+ // Find the first filename character (skip leading root path if any)
+ const size_t begin = zix_path_root_path_range(path.data).end;
+ if (begin == path.length || is_dir_sep(path.data[path.length - 1U])) {
+ return zix_make_range(0U, 0U);
+ }
+
+ // Scan backwards to find the first character after the last separator
+ size_t f = path.length - 1U;
+ while (f > begin && !is_dir_sep(path.data[f - 1])) {
+ --f;
+ }
+
+ return zix_make_range(f, path.length);
+}
+
+static ZixIndexRange
+zix_path_stem_range(const ZixStringView path)
+{
+ const ZixIndexRange name = zix_path_filename_range(path);
+
+ ZixIndexRange stem = name;
+ if (!zix_is_empty_range(stem) &&
+ !zix_string_ranges_equal(path.data, stem, ".", one_range) &&
+ !zix_string_ranges_equal(path.data, stem, "..", two_range)) {
+ // Scan backwards to the last "." after the last directory separator
+ --stem.end;
+ while (stem.end > stem.begin && path.data[stem.end] != '.') {
+ --stem.end;
+ }
+ }
+
+ return zix_is_empty_range(stem) ? name : stem;
+}
+
+static ZixIndexRange
+zix_path_extension_range(const ZixStringView path)
+{
+ const ZixIndexRange stem = zix_path_stem_range(path);
+
+ return zix_is_empty_range(stem)
+ ? stem
+ : zix_make_range(stem.end, stem.end + path.length - stem.end);
+}
+
+char*
+zix_path_join(ZixAllocator* const allocator,
+ const char* const a,
+ const char* const b)
+{
+ const ZixStringView b_view = zix_optional_string(b);
+ if (!a || !a[0]) {
+ return zix_string_view_copy(allocator, b_view);
+ }
+
+ const ZixStringView a_view = zix_optional_string(a);
+ const ZixRootSlices a_root = zix_path_root_slices(a);
+ const ZixRootSlices b_root = zix_path_root_slices(b);
+
+#ifdef _WIN32
+ // If the RHS has a different root name, just copy it
+ if (b_root.name.end &&
+ !zix_string_ranges_equal(a, a_root.name, b, b_root.name)) {
+ return zix_string_view_copy(allocator, b_view);
+ }
+#endif
+
+ // Determine how to join paths
+ const bool a_has_root_dir = !zix_is_empty_range(a_root.dir);
+ const bool a_has_filename = zix_path_has_filename(a_view.data);
+ size_t prefix_len = a_view.length;
+ bool add_sep = false;
+ if (!zix_is_empty_range(b_root.dir)) {
+ prefix_len = a_root.name.end; // Omit any path from a
+ } else if (a_has_filename || (!a_has_root_dir && zix_path_is_absolute(a))) {
+ add_sep = true; // Add a preferred separator after a
+ }
+
+ const size_t path_len = prefix_len + (size_t)add_sep + b_view.length;
+ char* const path = (char*)zix_calloc(allocator, path_len + 1U, 1U);
+
+ if (path) {
+ memcpy(path, a, prefix_len);
+
+ size_t p = prefix_len;
+ if (add_sep) {
+ path[p++] = ZIX_DIR_SEP;
+ }
+
+ if (b_view.length > b_root.name.end) {
+ memcpy(path + p, b + b_root.name.end, b_view.length - b_root.name.end);
+ path[p + b_view.length] = '\0';
+ }
+ }
+
+ return path;
+}
+
+char*
+zix_path_preferred(ZixAllocator* const allocator, const char* const path)
+{
+ const ZixStringView path_view = zix_string(path);
+ char* const result = (char*)zix_calloc(allocator, path_view.length + 1U, 1U);
+
+ if (result) {
+ for (size_t i = 0U; i < path_view.length; ++i) {
+ result[i] = (char)(is_dir_sep(path[i]) ? ZIX_DIR_SEP : path[i]);
+ }
+ }
+
+ return result;
+}
+
+char*
+zix_path_lexically_normal(ZixAllocator* const allocator, const char* const path)
+{
+ static const char sep = ZIX_DIR_SEP;
+
+ /* Loosely following the normalization algorithm from
+ <https://en.cppreference.com/w/cpp/filesystem/path>, but in such a way
+ that only a single buffer (the result) is needed. This means that
+ dot-dot segments are relatively expensive, but it beats a stack in
+ everyday common cases. */
+
+ if (!path[0]) {
+ return (char*)zix_calloc(allocator, 1U, 1U); // The empty path is normal
+ }
+
+ // Allocate a result buffer, with space for one additional character
+ const ZixStringView path_view = zix_string(path);
+ const size_t path_len = path_view.length;
+ char* const result = (char*)zix_calloc(allocator, path_len + 2U, 1);
+ size_t r = 0U;
+
+ // Copy root, normalizing separators as we go
+ const ZixIndexRange root = zix_path_root_path_range(path);
+ const size_t root_len = root.end - root.begin;
+ for (size_t i = 0; i < root_len; ++i) {
+ result[r++] = (char)(is_dir_sep(path[i]) ? sep : path[i]);
+ }
+
+ // Copy path, removing dot entries and collapsing separators as we go
+ for (size_t i = root.end; i < path_len; ++i) {
+ if (is_dir_sep(path[i])) {
+ if ((i >= root.end) && ((r == root.end + 1U && result[r - 1] == '.') ||
+ (r >= root.end + 2U && result[r - 2] == sep &&
+ result[r - 1] == '.'))) {
+ // Remove dot entry and any immediately following separators
+ result[--r] = '\0';
+
+ } else {
+ // Replace separators with a single preferred separator
+ result[r++] = sep;
+ }
+
+ // Collapse redundant separators
+ while (is_dir_sep(path[i + 1])) {
+ ++i;
+ }
+
+ } else {
+ result[r++] = path[i];
+ }
+ }
+
+ // Collapse any dot-dot entries following a directory name
+ size_t last = r;
+ size_t next = 0;
+ for (size_t i = root_len; i < r;) {
+ if (last < r && i > 2 && i + 1 <= r && result[i - 2] == sep &&
+ result[i - 1] == '.' && result[i] == '.' &&
+ (!result[i + 1] || is_dir_sep(result[i + 1]))) {
+ if (i < r && result[i + 1] == sep) {
+ ++i;
+ }
+
+ const size_t suffix_len = r - i - 1U;
+ memmove(result + last, result + i + 1, suffix_len);
+ r = r - ((r - last) - suffix_len);
+ result[r] = '\0';
+ i = 0;
+ last = r;
+ next = 0;
+ } else {
+ if (i >= 1 && result[i - 1] == sep) {
+ next = i;
+ }
+
+ if (result[i] != sep && result[i] != '.') {
+ last = next;
+ }
+ ++i;
+ }
+ }
+
+ // Remove any dot-dot entries following the root directory
+ if (root_len && is_dir_sep(result[root_len - 1U])) {
+ size_t start = root_len;
+ while (start < r && result[start] == '.' && result[start + 1] == '.' &&
+ (result[start + 2] == sep || result[start + 2] == '\0')) {
+ start += (result[start + 2] == sep) ? 3U : 2U;
+ }
+
+ if (start > root_len) {
+ if (start < r) {
+ memmove(result + root_len, result + start, r - start);
+ r = root_len + r - start;
+ } else {
+ r = root_len;
+ }
+
+ result[r] = '\0';
+ return result;
+ }
+ }
+
+ // Remove trailing dot entry
+ if (r >= 2U && is_any_sep(result[r - 2]) && result[r - 1] == '.') {
+ result[r - 1] = '\0';
+ }
+
+ // Remove trailing dot-dot entry
+ if (r >= 3U && result[r - 3] == '.' && result[r - 2] == '.' &&
+ is_any_sep(result[r - 1])) {
+ result[r - 1] = '\0';
+ }
+
+ // If the path is empty, add a dot
+ if (!result[0]) {
+ result[0] = '.';
+ result[1] = '\0';
+ }
+
+ return result;
+}
+
+static ZixPathIter
+make_iter(const ZixPathIterState state, const ZixIndexRange range)
+{
+ const ZixPathIter result = {range, state};
+ return result;
+}
+
+ZixPathIter
+zix_path_begin(const char* const path)
+{
+ const ZixPathIter iter = {zix_path_root_name_range(path), ZIX_PATH_ROOT_NAME};
+
+ return (iter.range.end > iter.range.begin) ? iter
+ : path ? zix_path_next(path, iter)
+ : zix_path_next("", iter);
+}
+
+ZixPathIter
+zix_path_next(const char* const path, ZixPathIter iter)
+{
+ if (iter.state == ZIX_PATH_ROOT_NAME) {
+ if (is_dir_sep(path[iter.range.end])) {
+ return make_iter(ZIX_PATH_ROOT_DIRECTORY,
+ zix_make_range(iter.range.end, iter.range.end + 1U));
+ }
+ }
+
+ if (iter.state <= ZIX_PATH_ROOT_DIRECTORY) {
+ iter.range.begin = iter.range.end;
+ iter.state = ZIX_PATH_FILE_NAME;
+ }
+
+ if (iter.state == ZIX_PATH_FILE_NAME) {
+ // If the last range end was the end of the path, we're done
+ iter.range.begin = iter.range.end;
+ if (!path[iter.range.begin]) {
+ return make_iter(ZIX_PATH_END, iter.range);
+ }
+
+ // Skip any leading separators
+ while (is_dir_sep(path[iter.range.begin])) {
+ iter.range.begin = ++iter.range.end;
+ }
+
+ // Advance end to the next separator or path end
+ while (path[iter.range.end] && !is_dir_sep(path[iter.range.end])) {
+ ++iter.range.end;
+ }
+ }
+
+ return iter;
+}
+
+static size_t
+zix_path_append(char* const buf,
+ const size_t offset,
+ const char* const string,
+ const size_t length)
+{
+ size_t o = offset;
+ if (offset) {
+ buf[o++] = ZIX_DIR_SEP;
+ }
+
+ memcpy(buf + o, string, length);
+ return o + length;
+}
+
+char*
+zix_path_lexically_relative(ZixAllocator* const allocator,
+ const char* const path,
+ const char* const base)
+{
+ // Mismatched roots can't be expressed in relative form
+ const ZixRootSlices path_root = zix_path_root_slices(path);
+ const ZixRootSlices base_root = zix_path_root_slices(base);
+ const bool path_has_root_dir = !zix_is_empty_range(path_root.dir);
+ const bool base_has_root_dir = !zix_is_empty_range(base_root.dir);
+ if (!zix_string_ranges_equal(path, path_root.name, base, base_root.name) ||
+ (zix_path_is_absolute(path) != zix_path_is_absolute(base)) ||
+ (!path_has_root_dir && base_has_root_dir)) {
+ return NULL;
+ }
+
+ // Find the first mismatching element in the paths (or the end)
+ ZixPathIter a = zix_path_begin(path);
+ ZixPathIter b = zix_path_begin(base);
+ while (a.state != ZIX_PATH_END && b.state != ZIX_PATH_END &&
+ a.state == b.state &&
+ zix_string_ranges_equal(path, a.range, base, b.range)) {
+ a = zix_path_next(path, a);
+ b = zix_path_next(base, b);
+ }
+
+ // Matching paths reduce to "."
+ if ((a.state == ZIX_PATH_END && b.state == ZIX_PATH_END) ||
+ (zix_is_empty_range(a.range) && b.state == ZIX_PATH_END)) {
+ return zix_string_view_copy(allocator, zix_string("."));
+ }
+
+ // Count the trailing non-matching entries in base
+ size_t n_base_up = 0U;
+ size_t n_non_empty = 0U;
+ for (; b.state < ZIX_PATH_END; b = zix_path_next(base, b)) {
+ if (!zix_is_empty_range(b.range)) {
+ if (zix_string_ranges_equal(base, b.range, "..", two_range)) {
+ ++n_base_up;
+ } else if (!zix_string_ranges_equal(base, b.range, ".", one_range)) {
+ ++n_non_empty;
+ }
+ }
+ }
+
+ // Base can't have too many up-references
+ if (n_base_up > n_non_empty) {
+ return NULL;
+ }
+
+ const size_t n_up =
+ (a.state == ZIX_PATH_ROOT_DIRECTORY) ? 0U : (n_non_empty - n_base_up);
+
+ // A result with no up-references or names reduces to "."
+ if (n_up == 0 && a.state == ZIX_PATH_END) {
+ return zix_string_view_copy(allocator, zix_string("."));
+ }
+
+ // Allocate buffer for relative path result
+ const size_t path_len = strlen(path);
+ const size_t rel_len = (n_up * 3U) + path_len - a.range.begin;
+ char* const rel = (char*)zix_calloc(allocator, rel_len + 1U, 1U);
+
+ // Write leading up-references
+ size_t offset = 0U;
+ for (size_t i = 0U; i < n_up; ++i) {
+ offset = zix_path_append(rel, offset, "..", 2U);
+ }
+
+ const char path_last = path[path_len - 1U];
+ if (a.range.begin < path_len) {
+ // Copy suffix from path (from `a` to the end)
+ const size_t suffix_len = path_len - a.range.begin;
+ offset = zix_path_append(rel, offset, path + a.range.begin, suffix_len);
+ } else if (n_up && path_len > 1 && is_dir_sep(path_last)) {
+ // Copy trailing directory separator from path
+ rel[offset++] = path_last;
+ }
+
+ rel[offset++] = '\0';
+ return rel;
+}
+
+// Decomposition
+
+static ZixStringView
+range_string_view(const char* const path, const ZixIndexRange range)
+{
+ return zix_substring(path + range.begin, range.end - range.begin);
+}
+
+#ifdef _WIN32
+
+ZixStringView
+zix_path_root_name(const char* const path)
+{
+ return range_string_view(path, zix_path_root_name_range(path));
+}
+
+#else
+
+ZixStringView
+zix_path_root_name(const char* const path)
+{
+ (void)path;
+ return zix_empty_string();
+}
+
+#endif
+
+ZixStringView
+zix_path_root_directory(const char* const path)
+{
+ return range_string_view(path, zix_path_root_slices(path).dir);
+}
+
+ZixStringView
+zix_path_root_path(const char* const path)
+{
+ return range_string_view(path, zix_path_root_path_range(path));
+}
+
+ZixStringView
+zix_path_relative_path(const char* const path)
+{
+ const ZixStringView path_view = zix_string(path);
+ const size_t path_len = path_view.length;
+ const ZixIndexRange root = zix_path_root_path_range(path);
+
+ return range_string_view(path, zix_make_range(root.end, path_len));
+}
+
+ZixStringView
+zix_path_parent_path(const char* const path)
+{
+ return range_string_view(path, zix_path_parent_path_range(zix_string(path)));
+}
+
+ZixStringView
+zix_path_filename(const char* const path)
+{
+ return range_string_view(path, zix_path_filename_range(zix_string(path)));
+}
+
+ZixStringView
+zix_path_stem(const char* const path)
+{
+ return range_string_view(path, zix_path_stem_range(zix_string(path)));
+}
+
+ZixStringView
+zix_path_extension(const char* const path)
+{
+ return range_string_view(path,
+ zix_path_extension_range(zix_optional_string(path)));
+}
+
+// Queries
+
+bool
+zix_path_has_root_path(const char* const path)
+{
+ return !zix_is_empty_range(zix_path_root_path_range(path));
+}
+
+bool
+zix_path_has_root_name(const char* const path)
+{
+ return !zix_is_empty_range(zix_path_root_name_range(path));
+}
+
+bool
+zix_path_has_root_directory(const char* const path)
+{
+ return !zix_is_empty_range(zix_path_root_slices(path).dir);
+}
+
+bool
+zix_path_has_relative_path(const char* const path)
+{
+ return path && path[zix_path_root_path_range(path).end];
+}
+
+bool
+zix_path_has_parent_path(const char* const path)
+{
+ return !zix_is_empty_range(
+ zix_path_parent_path_range(zix_optional_string(path)));
+}
+
+bool
+zix_path_has_filename(const char* const path)
+{
+ return !zix_is_empty_range(
+ zix_path_filename_range(zix_optional_string(path)));
+}
+
+bool
+zix_path_has_stem(const char* const path)
+{
+ return !zix_is_empty_range(zix_path_stem_range(zix_optional_string(path)));
+}
+
+bool
+zix_path_has_extension(const char* const path)
+{
+ return !zix_is_empty_range(
+ zix_path_extension_range(zix_optional_string(path)));
+}
+
+bool
+zix_path_is_absolute(const char* const path)
+{
+#ifdef _WIN32
+ const ZixRootSlices root = zix_path_root_slices(path);
+ return (!zix_is_empty_range(root.name) &&
+ (!zix_is_empty_range(root.dir) ||
+ (is_dir_sep(path[0]) && is_dir_sep(path[1]))));
+
+#else
+ return path && is_dir_sep(path[0]);
+#endif
+}
+
+bool
+zix_path_is_relative(const char* const path)
+{
+ return !zix_path_is_absolute(path);
+}
diff --git a/src/path_iter.h b/src/path_iter.h
new file mode 100644
index 0000000..b3b23e8
--- /dev/null
+++ b/src/path_iter.h
@@ -0,0 +1,31 @@
+// Copyright 2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef ZIX_PATH_ITER_H
+#define ZIX_PATH_ITER_H
+
+#include "index_range.h"
+
+#include "zix/attributes.h"
+
+typedef enum {
+ ZIX_PATH_ROOT_NAME,
+ ZIX_PATH_ROOT_DIRECTORY,
+ ZIX_PATH_FILE_NAME,
+ ZIX_PATH_END,
+} ZixPathIterState;
+
+typedef struct {
+ ZixIndexRange range;
+ ZixPathIterState state;
+} ZixPathIter;
+
+ZIX_PURE_FUNC
+ZixPathIter
+zix_path_begin(const char* ZIX_NULLABLE path);
+
+ZIX_PURE_FUNC
+ZixPathIter
+zix_path_next(const char* ZIX_NONNULL path, ZixPathIter iter);
+
+#endif // ZIX_PATH_ITER_H
diff --git a/test/cpp/.clang-tidy b/test/cpp/.clang-tidy
new file mode 100644
index 0000000..9a3cd8c
--- /dev/null
+++ b/test/cpp/.clang-tidy
@@ -0,0 +1,13 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ -*-avoid-c-arrays,
+ -*-no-malloc,
+ -android-cloexec-fopen,
+ -cppcoreguidelines-owning-memory,
+ -fuchsia-default-arguments-calls,
+ -modernize-raw-string-literal,
+ -modernize-use-trailing-return-type,
+ -readability-implicit-bool-conversion,
+InheritParentConfig: true
diff --git a/test/cpp/test_path_std.cpp b/test/cpp/test_path_std.cpp
new file mode 100644
index 0000000..f6767a6
--- /dev/null
+++ b/test/cpp/test_path_std.cpp
@@ -0,0 +1,496 @@
+// Copyright 2020-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+/*
+ Tests that compare results with std::filesystem::path. Also serves as a
+ handy record of divergence between zix, the C++ standard library, and
+ different implementations of it. The inconsistencies are confined to
+ Windows.
+*/
+
+#undef NDEBUG
+
+#include "zix/path.h"
+#include "zix/string_view.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <filesystem>
+#include <sstream>
+#include <string>
+
+struct BinaryCase {
+ const char* lhs;
+ const char* rhs;
+};
+
+static const BinaryCase joins[] = {
+ {"", ""}, {"", "/b/"}, {"", "b"}, {"/", ""},
+ {"..", ".."}, {"..", "name"}, {"..", "/"}, {"/", ".."},
+ {"/", nullptr}, {"//host", ""}, {"//host", "a"}, {"//host/", "a"},
+ {"/a", ""}, {"/a", "/b"}, {"/a", "/b/"}, {"/a", "b"},
+ {"/a", "b/"}, {"/a", nullptr}, {"/a/", ""}, {"/a/", "b"},
+ {"/a/", "b/"}, {"/a/", nullptr}, {"/a/b", nullptr}, {"/a/b/", ""},
+ {"/a/b/", nullptr}, {"/a/c", "b"}, {"/a/c/", "/b/d"}, {"/a/c/", "b"},
+ {"C:", ""}, {"C:/a", "/b"}, {"C:/a", "C:/b"}, {"C:/a", "C:b"},
+ {"C:/a", "D:b"}, {"C:/a", "b"}, {"C:/a", "b/"}, {"C:/a", "c:/b"},
+ {"C:/a", "c:b"}, {"C:\\a", "b"}, {"C:\\a", "b\\"}, {"C:a", "/b"},
+ {"C:a", "C:/b"}, {"C:a", "C:\\b"}, {"C:a", "C:b"}, {"C:a", "b"},
+ {"C:a", "c:/b"}, {"C:a", "c:\\b"}, {"C:a", "c:b"}, {"a", ""},
+ {"a", "/b"}, {"a", "C:"}, {"a", "C:/b"}, {"a", "C:\\b"},
+ {"a", "\\b"}, {"a", "b"}, {"a", "b/"}, {"a", nullptr},
+ {"a/", ""}, {"a/", "/b"}, {"a/", "b"}, {"a/", "b/"},
+ {"a/", nullptr}, {"a/b", ""}, {"a/b", nullptr}, {"a/b/", ""},
+ {"a/b/", nullptr}, {"a\\", "\\b"}, {"a\\", "b"}, {nullptr, "/b"},
+ {nullptr, "/b/c"}, {nullptr, "b"}, {nullptr, "b/c"}, {nullptr, nullptr},
+};
+
+static const BinaryCase lexical_relatives[] = {
+ {nullptr, nullptr},
+
+ {"", ""},
+ {"", "."},
+ {".", "."},
+ {"../", "../"},
+ {"../", "./"},
+ {"../", "a"},
+ {"../../a", "../b"},
+ {"../a", "../b"},
+ {"/", "/a/b"},
+ {"/", "/a/b/c"},
+ {"/", "a"},
+ {"/", "a/b"},
+ {"/", "a/b/c"},
+ {"//host", "//host"},
+ {"//host/", "//host/"},
+ {"//host/a/b", "//host/a/b"},
+ {"/a", "/b/c/"},
+ {"/a/", "/a/b"},
+ {"/a/", "/b/c"},
+ {"/a/", "/b/c/"},
+ {"/a/", "b"},
+ {"/a/", "b/c/"},
+ {"/a/b", "a/b"},
+ {"/a/b/c", "/a/b/d/"},
+ {"/a/b/c", "/a/b/d/e/"},
+ {"/a/b/c", "/a/d"},
+ {"C:", "C:a.txt"},
+ {"C:", "D:a.txt"},
+ {"C:../", "C:../"},
+ {"C:../", "C:./"},
+ {"C:../", "C:a"},
+ {"C:../../a", "C:../b"},
+ {"C:../a", "C:../b"},
+ {"C:/", "C:a.txt"},
+ {"C:/", "D:a.txt"},
+ {"C:/D/", "C:F"},
+ {"C:/D/", "C:F.txt"},
+ {"C:/D/S/", "F"},
+ {"C:/D/S/", "F.txt"},
+ {"C:/Dir/", "C:/Dir/File.txt"},
+ {"C:/Dir/", "C:File.txt"},
+ {"C:/Dir/File.txt", "C:/Dir/Sub/"},
+ {"C:/Dir/File.txt", "C:/Other.txt"},
+ {"C:/Dir/Sub/", "File.txt"},
+ {"C:/a.txt", "C:/b/c.txt"},
+ {"C:/a/", "C:/a/b.txt"},
+ {"C:/a/", "C:b.txt"},
+ {"C:/a/b.txt", "C:/a/b/"},
+ {"C:/a/b.txt", "C:/d.txt"},
+ {"C:/a/b/", "d.txt"},
+ {"C:/b/", "C:a.txt"},
+ {"C:/b/c/", "a.txt"},
+ {"C:F", "D:G"},
+ {"C:File.txt", "D:Other.txt"},
+ {"C:a.txt", "C:b.txt"},
+ {"C:a.txt", "D:"},
+ {"C:a.txt", "D:/"},
+ {"C:a.txt", "D:e.txt"},
+ {"C:a/b/c", "C:../"},
+ {"C:a/b/c", "C:../../"},
+ {"a", "a"},
+ {"a", "a/b/c"},
+ {"a", "b/c"},
+ {"a/b", "a/b"},
+ {"a/b", "c/d"},
+ {"a/b/c", "../"},
+ {"a/b/c", "../../"},
+ {"a/b/c", "a/b/c"},
+ {"a/b/c", "a/b/c/x/y"},
+ {"a/b/c/", "a/b/c/"},
+
+// Network paths that aren't supported by MinGW
+#if !(defined(_WIN32) && defined(__GLIBCXX__))
+ {"//host", "//host/"},
+ {"//host", "a"},
+ {"//host", "a"},
+ {"//host/", "a"},
+ {"//host/", "a"},
+#endif
+};
+
+static const char* const paths[] = {
+ // Valid paths handled consistently on all platforms
+ "",
+ ".",
+ "..",
+ "../",
+ "../..",
+ "../../",
+ "../../a",
+ "../a",
+ "../name",
+ "..\\..\\a",
+ "..\\a",
+ "..name",
+ "./",
+ "./.",
+ "./..",
+ ".//a//.//./b/.//",
+ "./a/././b/./",
+ "./name",
+ ".hidden",
+ ".hidden.txt",
+ "/",
+ "/.",
+ "/..",
+ "/../",
+ "/../..",
+ "/../../",
+ "/../a",
+ "/../a/../..",
+ "//",
+ "///",
+ "///dir/",
+ "///dir///",
+ "///dir///name",
+ "///dir///sub/////",
+ "///name",
+ "/a",
+ "/a/",
+ "/a/b",
+ "/a/b/",
+ "/a\\",
+ "/a\\b",
+ "/a\\b/",
+ "/dir/",
+ "/dir/.",
+ "/dir/..",
+ "/dir/../..",
+ "/dir/.hidden",
+ "/dir//name",
+ "/dir//sub/suub/",
+ "/dir/name",
+ "/dir/name.tar.gz",
+ "/dir/name.txt",
+ "/dir/name\\",
+ "/dir/sub/",
+ "/dir/sub/./",
+ "/dir/sub//",
+ "/dir/sub///",
+ "/dir/sub//name",
+ "/dir/sub/name",
+ "/dir/sub/suub/",
+ "/dir/sub/suub/../",
+ "/dir/sub/suub/../d",
+ "/dir/sub/suub/../d/",
+ "/dir/sub/suub/suuub/",
+ "/dir/sub\\name",
+ "/name",
+ "/name.",
+ "/name.txt",
+ "/name.txt.",
+ "C:",
+ "C:.",
+ "C:..",
+ "C:/",
+ "C:/.",
+ "C:/..",
+ "C:/../../name",
+ "C:/../name",
+ "C:/..dir/..name",
+ "C:/..name",
+ "C:/./name",
+ "C:/.dir/../name",
+ "C:/.dir/.hidden",
+ "C:/.hidden",
+ "C:/dir/",
+ "C:/dir/.",
+ "C:/dir/..",
+ "C:/dir/name",
+ "C:/dir/sub/",
+ "C:/dir/sub/name",
+ "C:/dir/sub/suub/",
+ "C:/dir/sub/suub/suuub/",
+ "C:/name",
+ "C:/name.",
+ "C:/name.txt",
+ "C:/name\\horror",
+ "C:/name\\horror/",
+ "C:\\",
+ "C:\\a",
+ "C:\\a/",
+ "C:\\a/b",
+ "C:\\a/b\\",
+ "C:\\a\\",
+ "C:\\a\\b",
+ "C:\\a\\b\\",
+ "C:\\a\\b\\c",
+ "C:\\a\\b\\d\\",
+ "C:\\a\\b\\d\\e\\",
+ "C:\\b",
+ "C:\\b\\c\\",
+ "C:a",
+ "C:dir/",
+ "C:dir/name",
+ "C:dir\\",
+ "C:name",
+ "C:name/",
+ "C:name\\horror",
+ "D|\\dir\\name",
+ "D|\\name",
+ "D|name",
+ "Z:/a/b",
+ "Z:\\a\\b",
+ "Z:b",
+ "\\",
+ "\\a\\/b\\/c\\",
+ "\\a\\b\\c\\",
+ "\\b",
+ "a",
+ "a.",
+ "a..txt",
+ "a.txt",
+ "a/b",
+ "a/b\\",
+ "c:/name",
+ "c:\\name",
+ "c:name",
+ "dir/",
+ "dir/.",
+ "dir/..",
+ "dir/../",
+ "dir/../b",
+ "dir/../b/../..///",
+ "dir/../b/..//..///../",
+ "dir/../sub/../name",
+ "dir/./",
+ "dir/.///b/../",
+ "dir/./b",
+ "dir/./b/.",
+ "dir/./b/..",
+ "dir/./sub/./name",
+ "dir///b",
+ "dir//b",
+ "dir/\\b",
+ "dir/b",
+ "dir/b.",
+ "dir/name",
+ "dir/name.",
+ "dir/name.tar.gz",
+ "dir/name.txt",
+ "dir/name.txt.",
+ "dir/name\\with\\backslashes",
+ "dir/sub/",
+ "dir/sub/.",
+ "dir/sub/..",
+ "dir/sub/../",
+ "dir/sub/../..",
+ "dir/sub/../../",
+ "dir/sub/../name",
+ "dir/sub/./",
+ "dir/sub/./..",
+ "dir/sub/./../",
+ "dir/sub/./name",
+ "dir/sub//",
+ "dir/sub///",
+ "dir/sub/name",
+ "dir/sub/suub/../..",
+ "dir/sub/suub/../../",
+ "dir/weird<sub/weird%name",
+ "dir\\",
+ "dir\\/\\a",
+ "dir\\/a",
+ "dir\\a",
+ "dir\\a/",
+ "name",
+ "name-snakey",
+ "name.",
+ "name..",
+ "name.txt",
+ "name.txt.",
+ "name/",
+ "name//",
+ "name///",
+ "name_sneaky",
+
+ // Filenames with colons that are handled consistently everywhere
+ "C:/a/C:/b",
+ "C:/a/c:/b",
+ "C:a/C:/b",
+ "C:a/C:\\b",
+ "C:a/c:/b",
+ "C:a/c:\\b",
+
+#if !defined(_WIN32)
+ // Filenames with colons that aren't handled consistently on Windows
+ "C:/a/C:b",
+ "C:/a/D:b",
+ "C:/a/c:b",
+ "C:a/C:b",
+ "C:a/c:b",
+ "NO:DRIVE",
+ "NODRIVE:",
+ "dir/C:",
+ "dir/C:/name",
+ "dir/C:\\name",
+ "no:drive",
+ "nodrive:",
+#endif
+
+#if !defined(_WIN32)
+ // Invalid root and network paths that aren't handled consistently on Windows
+ "//a//",
+ "//host/dir/../../../a",
+ "//host/dir/../../a",
+ "C://",
+#endif
+
+#if !(defined(_WIN32) && defined(__GLIBCXX__))
+ // Network paths that aren't supported by MinGW
+ "//h",
+ "//h/",
+ "//h/a",
+ "//h/a.txt",
+ "//h/d/a.txt",
+ "//h/d/s/a.txt",
+ "//host",
+ "//host/",
+ "//host/.",
+ "//host/..",
+ "//host/../dir/",
+ "//host/../name",
+ "//host/..dir/",
+ "//host/..name",
+ "//host/./dir/",
+ "//host/./name",
+ "//host/.dir/",
+ "//host/.name",
+ "//host/C:/dir/name.txt",
+ "//host/C:/name",
+ "//host/dir/.",
+ "//host/dir/..",
+ "//host/dir/../name",
+ "//host/dir/./name",
+ "//host/dir/name",
+ "//host/dir/sub/name",
+ "//host/name",
+ "//host/name.",
+ "//host/name..",
+ "//host\\",
+ "//host\\name.txt",
+ "\\\\host",
+ "\\\\host\\",
+ "\\\\host\\C:\\name",
+ "\\\\host\\dir\\name.txt",
+ "\\\\host\\name.txt",
+#endif
+};
+
+/// Abort if `path` doesn't match `result` when loosely parsed
+static bool
+match(const std::filesystem::path& path, char* const result)
+{
+ const bool success = (path.empty() && !result) || (result && path == result);
+
+ free(result);
+ return success;
+}
+
+/// Abort if `path` doesn't equal `result`
+static bool
+equal(const std::filesystem::path& path, char* const result)
+{
+ const bool success =
+ (path.empty() && !result) || (result && path.u8string() == result);
+
+ free(result);
+ return success;
+}
+
+/// Abort if `path` doesn't match `view` when loosely parsed
+static bool
+match(const std::filesystem::path& path, const ZixStringView view)
+{
+ return (path.empty() && !view.length) ||
+ (view.length && path == std::string{view.data, view.length});
+}
+
+/// Abort if `path` doesn't equal `view`
+static bool
+equal(const std::filesystem::path& path, const ZixStringView view)
+{
+ return (path.empty() && !view.length) ||
+ (view.length &&
+ path.u8string() == std::string{view.data, view.length});
+}
+
+int
+main()
+{
+ using Path = std::filesystem::path;
+
+ for (const char* string : paths) {
+ const Path path{string};
+
+ // Decomposition
+ assert(equal(path.root_name(), zix_path_root_name(string)));
+ assert(match(path.root_directory(), zix_path_root_directory(string)));
+ assert(match(path.root_path(), zix_path_root_path(string)));
+ assert(equal(path.relative_path(), zix_path_relative_path(string)));
+ assert(match(path.parent_path(), zix_path_parent_path(string)));
+ assert(equal(path.filename(), zix_path_filename(string)));
+ assert(equal(path.stem(), zix_path_stem(string)));
+ assert(equal(path.extension(), zix_path_extension(string)));
+
+ // Queries
+ assert(path.has_root_path() == zix_path_has_root_path(string));
+ assert(path.has_root_name() == zix_path_has_root_name(string));
+ assert(path.has_root_directory() == zix_path_has_root_directory(string));
+ assert(path.has_relative_path() == zix_path_has_relative_path(string));
+ assert(path.has_parent_path() == zix_path_has_parent_path(string));
+ assert(path.has_filename() == zix_path_has_filename(string));
+ assert(path.has_stem() == zix_path_has_stem(string));
+ assert(path.has_extension() == zix_path_has_extension(string));
+ assert(path.is_absolute() == zix_path_is_absolute(string));
+ assert(path.is_relative() == zix_path_is_relative(string));
+
+ // Generation
+
+ assert(
+ equal(Path{path}.make_preferred(), zix_path_preferred(nullptr, string)));
+
+ assert(match(path.lexically_normal(),
+ zix_path_lexically_normal(nullptr, string)));
+ }
+
+ for (const auto& join : joins) {
+ const Path l = join.lhs ? Path{join.lhs} : Path{};
+ const Path r = join.rhs ? Path{join.rhs} : Path{};
+
+ assert(equal(l / r, zix_path_join(nullptr, join.lhs, join.rhs)));
+ }
+
+ for (const auto& relatives : lexical_relatives) {
+ const Path l = relatives.lhs ? Path{relatives.lhs} : Path{};
+ const Path r = relatives.rhs ? Path{relatives.rhs} : Path{};
+
+ assert(match(
+ l.lexically_relative(r),
+ zix_path_lexically_relative(nullptr, relatives.lhs, relatives.rhs)));
+
+ assert(match(
+ r.lexically_relative(l),
+ zix_path_lexically_relative(nullptr, relatives.rhs, relatives.lhs)));
+ }
+}
diff --git a/test/headers/test_headers.c b/test/headers/test_headers.c
index dc580ea..9e5bddd 100644
--- a/test/headers/test_headers.c
+++ b/test/headers/test_headers.c
@@ -9,6 +9,7 @@
#include "zix/digest.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
#include "zix/ring.h" // IWYU pragma: keep
#include "zix/sem.h" // IWYU pragma: keep
#include "zix/status.h" // IWYU pragma: keep
diff --git a/test/test_path.c b/test/test_path.c
new file mode 100644
index 0000000..5c98c32
--- /dev/null
+++ b/test/test_path.c
@@ -0,0 +1,948 @@
+// Copyright 2020-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#undef NDEBUG
+
+#include "zix/path.h"
+#include "zix/string_view.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+/// Abort if `string` doesn't equal `expected`
+static bool
+equal(char* const string, const char* const expected)
+{
+ const bool result =
+ (!string && !expected) || (string && expected && !strcmp(string, expected));
+
+ free(string);
+ return result;
+}
+
+/// Abort if `string` doesn't equal `expected` with preferred separators
+static bool
+match(char* const string, const char* const expected)
+{
+ char* const e = expected ? zix_path_preferred(NULL, expected) : NULL;
+ char* const s = string ? zix_path_preferred(NULL, string) : NULL;
+
+ const bool result = (!s && !e) || (s && e && !strcmp(s, e));
+
+ free(s);
+ free(e);
+ free(string);
+ return result;
+}
+
+/// Abort if `view` doesn't equal `expected`
+static bool
+view_equal(const ZixStringView view, const char* const expected)
+{
+ const bool result =
+ (!view.length && !expected) ||
+ (view.length && expected && strlen(expected) == view.length &&
+ !strncmp(view.data, expected, view.length));
+
+ return result;
+}
+
+static void
+test_path_join(void)
+{
+ // Trivial cases
+ assert(equal(zix_path_join(NULL, "", ""), ""));
+ assert(equal(zix_path_join(NULL, "", "/b/"), "/b/"));
+ assert(equal(zix_path_join(NULL, "", "b"), "b"));
+ assert(equal(zix_path_join(NULL, "/", ""), "/"));
+ assert(equal(zix_path_join(NULL, "/a/", ""), "/a/"));
+ assert(equal(zix_path_join(NULL, "/a/b/", ""), "/a/b/"));
+ assert(equal(zix_path_join(NULL, "a/", ""), "a/"));
+ assert(equal(zix_path_join(NULL, "a/b/", ""), "a/b/"));
+
+ // Null is treated like an empty path
+ assert(equal(zix_path_join(NULL, "/", NULL), "/"));
+ assert(equal(zix_path_join(NULL, "/a/", NULL), "/a/"));
+ assert(equal(zix_path_join(NULL, "/a/b/", NULL), "/a/b/"));
+ assert(equal(zix_path_join(NULL, "a/", NULL), "a/"));
+ assert(equal(zix_path_join(NULL, "a/b/", NULL), "a/b/"));
+ assert(equal(zix_path_join(NULL, NULL, "/b"), "/b"));
+ assert(equal(zix_path_join(NULL, NULL, "/b/c"), "/b/c"));
+ assert(equal(zix_path_join(NULL, NULL, "b"), "b"));
+ assert(equal(zix_path_join(NULL, NULL, "b/c"), "b/c"));
+ assert(equal(zix_path_join(NULL, NULL, NULL), ""));
+
+ // Joining an absolute path discards the left path
+ assert(equal(zix_path_join(NULL, "/a", "/b"), "/b"));
+ assert(equal(zix_path_join(NULL, "/a", "/b/"), "/b/"));
+ assert(equal(zix_path_join(NULL, "a", "/b"), "/b"));
+ assert(equal(zix_path_join(NULL, "a/", "/b"), "/b"));
+
+ // Concatenation cases
+ assert(equal(zix_path_join(NULL, "/a/", "b"), "/a/b"));
+ assert(equal(zix_path_join(NULL, "/a/", "b/"), "/a/b/"));
+ assert(equal(zix_path_join(NULL, "a/", "b"), "a/b"));
+ assert(equal(zix_path_join(NULL, "a/", "b/"), "a/b/"));
+ assert(equal(zix_path_join(NULL, "/a/c/", "/b/d"), "/b/d"));
+ assert(equal(zix_path_join(NULL, "/a/c/", "b"), "/a/c/b"));
+
+#ifdef _WIN32
+ // "C:" is a drive letter, "//host" is a share, backslash is a separator
+ assert(equal(zix_path_join(NULL, "//host", ""), "//host\\"));
+ assert(equal(zix_path_join(NULL, "//host", "a"), "//host\\a"));
+ assert(equal(zix_path_join(NULL, "//host/", "a"), "//host/a"));
+ assert(equal(zix_path_join(NULL, "/a", ""), "/a\\"));
+ assert(equal(zix_path_join(NULL, "/a", "b"), "/a\\b"));
+ assert(equal(zix_path_join(NULL, "/a", "b/"), "/a\\b/"));
+ assert(equal(zix_path_join(NULL, "/a", NULL), "/a\\"));
+ assert(equal(zix_path_join(NULL, "/a/b", NULL), "/a/b\\"));
+ assert(equal(zix_path_join(NULL, "/a/c", "b"), "/a/c\\b"));
+ assert(equal(zix_path_join(NULL, "C:", ""), "C:"));
+ assert(equal(zix_path_join(NULL, "C:/a", "/b"), "C:/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "C:/b"), "C:/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "C:b"), "C:/a\\b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "D:b"), "D:b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "b"), "C:/a\\b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "b/"), "C:/a\\b/"));
+ assert(equal(zix_path_join(NULL, "C:/a", "c:/b"), "c:/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "c:b"), "c:b"));
+ assert(equal(zix_path_join(NULL, "C:\\a", "b"), "C:\\a\\b"));
+ assert(equal(zix_path_join(NULL, "C:\\a", "b\\"), "C:\\a\\b\\"));
+ assert(equal(zix_path_join(NULL, "C:a", "/b"), "C:/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:/b"), "C:/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:\\b"), "C:\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:b"), "C:a\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "b"), "C:a\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:/b"), "c:/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:\\b"), "c:\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:b"), "c:b"));
+ assert(equal(zix_path_join(NULL, "a", ""), "a\\"));
+ assert(equal(zix_path_join(NULL, "a", "C:"), "C:"));
+ assert(equal(zix_path_join(NULL, "a", "C:/b"), "C:/b"));
+ assert(equal(zix_path_join(NULL, "a", "C:\\b"), "C:\\b"));
+ assert(equal(zix_path_join(NULL, "a", "\\b"), "\\b"));
+ assert(equal(zix_path_join(NULL, "a", "b"), "a\\b"));
+ assert(equal(zix_path_join(NULL, "a", "b/"), "a\\b/"));
+ assert(equal(zix_path_join(NULL, "a", NULL), "a\\"));
+ assert(equal(zix_path_join(NULL, "a/b", ""), "a/b\\"));
+ assert(equal(zix_path_join(NULL, "a/b", NULL), "a/b\\"));
+ assert(equal(zix_path_join(NULL, "a\\", "\\b"), "\\b"));
+ assert(equal(zix_path_join(NULL, "a\\", "b"), "a\\b"));
+#else
+ // "C:" isn't special, "//host" has extra slashes, slash is the only separator
+ assert(equal(zix_path_join(NULL, "//host", ""), "//host/"));
+ assert(equal(zix_path_join(NULL, "//host", "a"), "//host/a"));
+ assert(equal(zix_path_join(NULL, "//host/", "a"), "//host/a"));
+ assert(equal(zix_path_join(NULL, "/a", ""), "/a/"));
+ assert(equal(zix_path_join(NULL, "/a", "b"), "/a/b"));
+ assert(equal(zix_path_join(NULL, "/a", "b/"), "/a/b/"));
+ assert(equal(zix_path_join(NULL, "/a", NULL), "/a/"));
+ assert(equal(zix_path_join(NULL, "/a/b", NULL), "/a/b/"));
+ assert(equal(zix_path_join(NULL, "/a/c", "b"), "/a/c/b"));
+ assert(equal(zix_path_join(NULL, "C:", ""), "C:/"));
+ assert(equal(zix_path_join(NULL, "C:/a", "/b"), "/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "C:/b"), "C:/a/C:/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "C:b"), "C:/a/C:b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "D:b"), "C:/a/D:b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "b"), "C:/a/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "b/"), "C:/a/b/"));
+ assert(equal(zix_path_join(NULL, "C:/a", "c:/b"), "C:/a/c:/b"));
+ assert(equal(zix_path_join(NULL, "C:/a", "c:b"), "C:/a/c:b"));
+ assert(equal(zix_path_join(NULL, "C:\\a", "b"), "C:\\a/b"));
+ assert(equal(zix_path_join(NULL, "C:\\a", "b\\"), "C:\\a/b\\"));
+ assert(equal(zix_path_join(NULL, "C:a", "/b"), "/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:/b"), "C:a/C:/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:\\b"), "C:a/C:\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "C:b"), "C:a/C:b"));
+ assert(equal(zix_path_join(NULL, "C:a", "b"), "C:a/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:/b"), "C:a/c:/b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:\\b"), "C:a/c:\\b"));
+ assert(equal(zix_path_join(NULL, "C:a", "c:b"), "C:a/c:b"));
+ assert(equal(zix_path_join(NULL, "a", ""), "a/"));
+ assert(equal(zix_path_join(NULL, "a", "C:"), "a/C:"));
+ assert(equal(zix_path_join(NULL, "a", "C:/b"), "a/C:/b"));
+ assert(equal(zix_path_join(NULL, "a", "C:\\b"), "a/C:\\b"));
+ assert(equal(zix_path_join(NULL, "a", "\\b"), "a/\\b"));
+ assert(equal(zix_path_join(NULL, "a", "b"), "a/b"));
+ assert(equal(zix_path_join(NULL, "a", "b/"), "a/b/"));
+ assert(equal(zix_path_join(NULL, "a", NULL), "a/"));
+ assert(equal(zix_path_join(NULL, "a/b", ""), "a/b/"));
+ assert(equal(zix_path_join(NULL, "a/b", NULL), "a/b/"));
+ assert(equal(zix_path_join(NULL, "a\\", "\\b"), "a\\/\\b"));
+ assert(equal(zix_path_join(NULL, "a\\", "b"), "a\\/b"));
+#endif
+}
+
+static void
+test_path_preferred(void)
+{
+ assert(equal(zix_path_preferred(NULL, ""), ""));
+ assert(equal(zix_path_preferred(NULL, "some-name"), "some-name"));
+ assert(equal(zix_path_preferred(NULL, "some_name"), "some_name"));
+
+#ifdef _WIN32
+ // Backslash is the preferred separator
+ assert(equal(zix_path_preferred(NULL, "/"), "\\"));
+ assert(equal(zix_path_preferred(NULL, "/."), "\\."));
+ assert(equal(zix_path_preferred(NULL, "//a"), "\\\\a"));
+ assert(equal(zix_path_preferred(NULL, "//a/"), "\\\\a\\"));
+ assert(equal(zix_path_preferred(NULL, "//a//"), "\\\\a\\\\"));
+ assert(equal(zix_path_preferred(NULL, "/a//b/c/"), "\\a\\\\b\\c\\"));
+ assert(equal(zix_path_preferred(NULL, "/a/b/c/"), "\\a\\b\\c\\"));
+ assert(equal(zix_path_preferred(NULL, "\\"), "\\"));
+ assert(
+ equal(zix_path_preferred(NULL, "\\a\\//b\\/c\\"), "\\a\\\\\\b\\\\c\\"));
+ assert(equal(zix_path_preferred(NULL, "\\a\\/b\\/c\\"), "\\a\\\\b\\\\c\\"));
+ assert(equal(zix_path_preferred(NULL, "\\a\\b\\c\\"), "\\a\\b\\c\\"));
+#else
+ // Slash is the only separator
+ assert(equal(zix_path_preferred(NULL, "/"), "/"));
+ assert(equal(zix_path_preferred(NULL, "/."), "/."));
+ assert(equal(zix_path_preferred(NULL, "//a"), "//a"));
+ assert(equal(zix_path_preferred(NULL, "//a/"), "//a/"));
+ assert(equal(zix_path_preferred(NULL, "//a//"), "//a//"));
+ assert(equal(zix_path_preferred(NULL, "/a//b/c/"), "/a//b/c/"));
+ assert(equal(zix_path_preferred(NULL, "/a/b/c/"), "/a/b/c/"));
+ assert(equal(zix_path_preferred(NULL, "\\"), "\\"));
+ assert(equal(zix_path_preferred(NULL, "\\a\\//b\\/c\\"), "\\a\\//b\\/c\\"));
+ assert(equal(zix_path_preferred(NULL, "\\a\\/b\\/c\\"), "\\a\\/b\\/c\\"));
+ assert(equal(zix_path_preferred(NULL, "\\a\\b\\c\\"), "\\a\\b\\c\\"));
+#endif
+}
+
+static void
+test_path_lexically_normal(void)
+{
+ // Identities
+ assert(equal(zix_path_lexically_normal(NULL, ""), ""));
+ assert(equal(zix_path_lexically_normal(NULL, "."), "."));
+ assert(equal(zix_path_lexically_normal(NULL, ".."), ".."));
+ assert(match(zix_path_lexically_normal(NULL, "../.."), "../.."));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/"), "/a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/c"), "/a/b/c"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b"), "a/b"));
+
+ // "Out of bounds" leading dot-dot entries are removed
+ assert(match(zix_path_lexically_normal(NULL, "/../"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../.."), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../../"), "/"));
+
+ // Windows drive letters are preserved
+ assert(equal(zix_path_lexically_normal(NULL, "C:"), "C:"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:a"), "C:a"));
+ assert(match(zix_path_lexically_normal(NULL, "C:/"), "C:/"));
+ assert(match(zix_path_lexically_normal(NULL, "C:/a"), "C:/a"));
+ assert(match(zix_path_lexically_normal(NULL, "C:/a/"), "C:/a/"));
+ assert(match(zix_path_lexically_normal(NULL, "C:/a/b"), "C:/a/b"));
+ assert(match(zix_path_lexically_normal(NULL, "C:a/"), "C:a/"));
+
+ // Slashes are replaced with a single preferred-separator
+ assert(match(zix_path_lexically_normal(NULL, "/"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "//"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "///"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "///a///b/////"), "/a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b//c"), "/a/b/c"));
+ assert(match(zix_path_lexically_normal(NULL, "a///b"), "a/b"));
+ assert(match(zix_path_lexically_normal(NULL, "a//b"), "a/b"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b"), "a/b"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/"), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b//"), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b///"), "a/b/"));
+
+ // Dot directories are removed
+ assert(equal(zix_path_lexically_normal(NULL, "./.."), ".."));
+ assert(match(zix_path_lexically_normal(NULL, ".//a//.//./b/.//"), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "./a/././b/./"), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "/."), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/./"), "/a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/."), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/./b/."), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/."), "a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/./"), "a/b/"));
+
+ // Real entries before a dot-dot entry are removed
+ assert(equal(zix_path_lexically_normal(NULL, "a/.."), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "a/../"), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "a/b/../.."), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "a/b/../../"), "."));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/c/../"), "/a/b/"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/c/../d"), "/a/b/d"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/b/c/../d/"), "/a/b/d/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/.."), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/../"), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/./.."), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/./../"), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/c/../.."), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/b/c/../../"), "a/"));
+
+ // Both dot directories and dot-dot entries
+ assert(match(zix_path_lexically_normal(NULL, "a/.///b/../"), "a/"));
+ assert(match(zix_path_lexically_normal(NULL, "a/./b/.."), "a/"));
+
+ // Dot-dot entries after a root are removed
+ assert(match(zix_path_lexically_normal(NULL, "/.."), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../.."), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../../"), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/../a"), "/a"));
+ assert(match(zix_path_lexically_normal(NULL, "/../a/../.."), "/"));
+ assert(match(zix_path_lexically_normal(NULL, "/a/../.."), "/"));
+
+ // No trailing separator after a trailing dot-dot entry
+ assert(equal(zix_path_lexically_normal(NULL, "../"), ".."));
+ assert(match(zix_path_lexically_normal(NULL, "../../"), "../.."));
+ assert(match(zix_path_lexically_normal(NULL, "a/../b/../..///"), ".."));
+ assert(
+ match(zix_path_lexically_normal(NULL, "a/../b/..//..///../"), "../.."));
+
+ // Effectively empty paths are a dot
+ assert(equal(zix_path_lexically_normal(NULL, "."), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "./"), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "./."), "."));
+ assert(equal(zix_path_lexically_normal(NULL, "a/.."), "."));
+
+#ifdef _WIN32
+
+ // Paths like "C:/filename" have a drive letter
+ assert(equal(zix_path_lexically_normal(NULL, "C:/a\\b"), "C:\\a\\b"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\"), "C:\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a"), "C:\\a"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a/"), "C:\\a\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a\\"), "C:\\a\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:a\\"), "C:a\\"));
+
+ // Paths like "//host/dir/" have a network root
+ assert(equal(zix_path_lexically_normal(NULL, "//a/"), "\\\\a\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/.."), "\\\\a\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/b/"), "\\\\a\\b\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/b/."), "\\\\a\\b\\"));
+
+#else
+
+ // "C:" is just a strange directory or file name prefix
+ assert(equal(zix_path_lexically_normal(NULL, "C:/a\\b"), "C:/a\\b"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\"), "C:\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a"), "C:\\a"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a/"), "C:\\a/"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:\\a\\"), "C:\\a\\"));
+ assert(equal(zix_path_lexically_normal(NULL, "C:a\\"), "C:a\\"));
+
+ // Paths like "//host/dir/" have redundant leading slashes
+ assert(equal(zix_path_lexically_normal(NULL, "//a/"), "/a/"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/.."), "/"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/b/"), "/a/b/"));
+ assert(equal(zix_path_lexically_normal(NULL, "//a/b/."), "/a/b/"));
+
+#endif
+}
+
+static void
+test_path_lexically_relative(void)
+{
+ assert(equal(zix_path_lexically_relative(NULL, "", ""), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "", "."), "."));
+ assert(equal(zix_path_lexically_relative(NULL, ".", ""), "."));
+ assert(equal(zix_path_lexically_relative(NULL, ".", "."), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "//base", "a"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "//host", "//host"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "//host", "a"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "//host/"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "a"), NULL));
+ assert(
+ equal(zix_path_lexically_relative(NULL, "//host/a/b", "//host/a/b"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "/a/b", "/a/"), "b"));
+ assert(equal(zix_path_lexically_relative(NULL, "C:/a/b", "C:/a/"), "b"));
+ assert(equal(zix_path_lexically_relative(NULL, "a", "/"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "a", "//host"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "a", "//host/"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "a", "a"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "a/b", "/a/b"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "a/b", "a/b"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "a/b/c", "a"), "b/c"));
+ assert(equal(zix_path_lexically_relative(NULL, "a/b/c", "a/b/c"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "a/b/c/", "a/b/c/"), "."));
+ assert(match(zix_path_lexically_relative(NULL, "../", "../"), "."));
+ assert(match(zix_path_lexically_relative(NULL, "../", "./"), "../"));
+ assert(match(zix_path_lexically_relative(NULL, "../", "a"), "../../"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "../../a", "../b"), "../../a"));
+ assert(match(zix_path_lexically_relative(NULL, "../a", "../b"), "../a"));
+ assert(match(zix_path_lexically_relative(NULL, "/a", "/b/c/"), "../../a"));
+ assert(match(zix_path_lexically_relative(NULL, "/a/b/c", "/a/b/d/"), "../c"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "/a/b/c", "/a/b/d/e/"), "../../c"));
+ assert(match(zix_path_lexically_relative(NULL, "/a/b/c", "/a/d"), "../b/c"));
+ assert(match(zix_path_lexically_relative(NULL, "/a/d", "/a/b/c"), "../../d"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:/D/", "C:/D/F.txt"), "../"));
+ assert(match(zix_path_lexically_relative(NULL, "C:/D/F", "C:/D/S/"), "../F"));
+ assert(match(zix_path_lexically_relative(NULL, "C:/D/F", "C:/G"), "../D/F"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:/D/F.txt", "C:/D/"), "F.txt"));
+ assert(match(zix_path_lexically_relative(NULL, "C:/E", "C:/D/G"), "../../E"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:/a", "C:/b/c/"), "../../a"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:/a/b/c", "C:/a/b/d/"), "../c"));
+ assert(match(zix_path_lexically_relative(NULL, "a/b", "c/d"), "../../a/b"));
+ assert(match(zix_path_lexically_relative(NULL, "a/b/c", "../"), NULL));
+ assert(match(zix_path_lexically_relative(NULL, "a/b/c", "../../"), NULL));
+ assert(
+ match(zix_path_lexically_relative(NULL, "a/b/c", "a/b/c/x/y"), "../.."));
+
+#ifdef _WIN32
+ assert(equal(zix_path_lexically_relative(NULL, "/", "a"), "/"));
+ assert(equal(zix_path_lexically_relative(NULL, "//host", "//host/"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "//host"), "/"));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "/a"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "/a", "//host"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "/a", "//host/"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "C:/D/", "C:F.txt"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "C:/D/S/", "F.txt"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "C:F", "C:/D/"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "C:F", "D:G"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "F", "C:/D/S/"), NULL));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:../"), "."));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:./"), "../"));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:a"), "../../"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:../../a", "C:../b"), "../../a"));
+ assert(match(zix_path_lexically_relative(NULL, "C:../a", "C:../b"), "../a"));
+ assert(match(zix_path_lexically_relative(NULL, "C:a/b/c", "C:../"), NULL));
+ assert(match(zix_path_lexically_relative(NULL, "C:a/b/c", "C:../../"), NULL));
+#else
+ assert(equal(zix_path_lexically_relative(NULL, "/", "a"), NULL));
+ assert(equal(zix_path_lexically_relative(NULL, "//host", "//host/"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "//host"), "."));
+ assert(equal(zix_path_lexically_relative(NULL, "//host/", "/a"), "../host/"));
+ assert(equal(zix_path_lexically_relative(NULL, "/a", "//host"), "../a"));
+ assert(equal(zix_path_lexically_relative(NULL, "/a", "//host/"), "../a"));
+ assert(
+ equal(zix_path_lexically_relative(NULL, "C:/D/", "C:F.txt"), "../C:/D/"));
+ assert(
+ equal(zix_path_lexically_relative(NULL, "C:/D/S/", "F.txt"), "../C:/D/S/"));
+ assert(equal(zix_path_lexically_relative(NULL, "C:F", "C:/D/"), "../../C:F"));
+ assert(equal(zix_path_lexically_relative(NULL, "C:F", "D:G"), "../C:F"));
+ assert(
+ equal(zix_path_lexically_relative(NULL, "F", "C:/D/S/"), "../../../F"));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:../"), "."));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:./"), "../C:../"));
+ assert(match(zix_path_lexically_relative(NULL, "C:../", "C:a"), "../C:../"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:../../a", "C:../b"), "../../a"));
+ assert(match(zix_path_lexically_relative(NULL, "C:../a", "C:../b"), "../a"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:a/b/c", "C:../"), "../C:a/b/c"));
+ assert(
+ match(zix_path_lexically_relative(NULL, "C:a/b/c", "C:../../"), "C:a/b/c"));
+#endif
+}
+
+static void
+test_path_root_name(void)
+{
+ // Relative paths with no root
+ assert(view_equal(zix_path_root_name(""), NULL));
+ assert(view_equal(zix_path_root_name("."), NULL));
+ assert(view_equal(zix_path_root_name(".."), NULL));
+ assert(view_equal(zix_path_root_name("../"), NULL));
+ assert(view_equal(zix_path_root_name("./"), NULL));
+ assert(view_equal(zix_path_root_name("NONDRIVE:"), NULL));
+ assert(view_equal(zix_path_root_name("a"), NULL));
+ assert(view_equal(zix_path_root_name("a/"), NULL));
+ assert(view_equal(zix_path_root_name("a/."), NULL));
+ assert(view_equal(zix_path_root_name("a/.."), NULL));
+ assert(view_equal(zix_path_root_name("a/../"), NULL));
+ assert(view_equal(zix_path_root_name("a/../b"), NULL));
+ assert(view_equal(zix_path_root_name("a/./"), NULL));
+ assert(view_equal(zix_path_root_name("a/./b"), NULL));
+ assert(view_equal(zix_path_root_name("a/b"), NULL));
+
+ // Absolute paths with a POSIX-style root
+ assert(view_equal(zix_path_root_name("/"), NULL));
+ assert(view_equal(zix_path_root_name("/."), NULL));
+ assert(view_equal(zix_path_root_name("/.."), NULL));
+ assert(view_equal(zix_path_root_name("//"), NULL));
+ assert(view_equal(zix_path_root_name("///a///"), NULL));
+ assert(view_equal(zix_path_root_name("///a///b"), NULL));
+ assert(view_equal(zix_path_root_name("/a"), NULL));
+ assert(view_equal(zix_path_root_name("/a/"), NULL));
+ assert(view_equal(zix_path_root_name("/a//b"), NULL));
+
+#ifdef _WIN32
+
+ // Paths like "C:/filename" have a drive letter
+ assert(view_equal(zix_path_root_name("C:"), "C:"));
+ assert(view_equal(zix_path_root_name("C:/"), "C:"));
+ assert(view_equal(zix_path_root_name("C:/a"), "C:"));
+ assert(view_equal(zix_path_root_name("C:/a/"), "C:"));
+ assert(view_equal(zix_path_root_name("C:/a/b"), "C:"));
+ assert(view_equal(zix_path_root_name("C:a"), "C:"));
+ assert(view_equal(zix_path_root_name("C:a/"), "C:"));
+
+ // Paths like "//host/" are network roots
+ assert(view_equal(zix_path_root_name("//host"), "//host"));
+ assert(view_equal(zix_path_root_name("//host/"), "//host"));
+ assert(view_equal(zix_path_root_name("//host/a"), "//host"));
+
+ // Backslash is a directory separator
+ assert(view_equal(zix_path_root_name("C:/a\\b"), "C:"));
+ assert(view_equal(zix_path_root_name("C:\\"), "C:"));
+ assert(view_equal(zix_path_root_name("C:\\a"), "C:"));
+ assert(view_equal(zix_path_root_name("C:\\a/"), "C:"));
+ assert(view_equal(zix_path_root_name("C:\\a\\"), "C:"));
+ assert(view_equal(zix_path_root_name("C:a\\"), "C:"));
+
+#else
+
+ // "C:" is just a strange directory or file name prefix
+ assert(view_equal(zix_path_root_name("C:"), NULL));
+ assert(view_equal(zix_path_root_name("C:/"), NULL));
+ assert(view_equal(zix_path_root_name("C:/a"), NULL));
+ assert(view_equal(zix_path_root_name("C:/a/"), NULL));
+ assert(view_equal(zix_path_root_name("C:/a/b"), NULL));
+ assert(view_equal(zix_path_root_name("C:a"), NULL));
+ assert(view_equal(zix_path_root_name("C:a/"), NULL));
+
+ // Paths like "//host/" have redundant leading slashes
+ assert(view_equal(zix_path_root_name("//host"), NULL));
+ assert(view_equal(zix_path_root_name("//host/"), NULL));
+ assert(view_equal(zix_path_root_name("//host/a"), NULL));
+
+ // Backslash is a character in a directory or file name
+ assert(view_equal(zix_path_root_name("C:/a\\b"), NULL));
+ assert(view_equal(zix_path_root_name("C:\\"), NULL));
+ assert(view_equal(zix_path_root_name("C:\\a"), NULL));
+ assert(view_equal(zix_path_root_name("C:\\a/"), NULL));
+ assert(view_equal(zix_path_root_name("C:\\a\\"), NULL));
+ assert(view_equal(zix_path_root_name("C:a\\"), NULL));
+
+#endif
+}
+
+static void
+test_path_root(void)
+{
+ // Relative paths with no root
+ assert(view_equal(zix_path_root_path(""), NULL));
+ assert(view_equal(zix_path_root_path("."), NULL));
+ assert(view_equal(zix_path_root_path(".."), NULL));
+ assert(view_equal(zix_path_root_path("../"), NULL));
+ assert(view_equal(zix_path_root_path("./"), NULL));
+ assert(view_equal(zix_path_root_path("NONDRIVE:"), NULL));
+ assert(view_equal(zix_path_root_path("a"), NULL));
+ assert(view_equal(zix_path_root_path("a/"), NULL));
+ assert(view_equal(zix_path_root_path("a/."), NULL));
+ assert(view_equal(zix_path_root_path("a/.."), NULL));
+ assert(view_equal(zix_path_root_path("a/../"), NULL));
+ assert(view_equal(zix_path_root_path("a/../b"), NULL));
+ assert(view_equal(zix_path_root_path("a/./"), NULL));
+ assert(view_equal(zix_path_root_path("a/./b"), NULL));
+ assert(view_equal(zix_path_root_path("a/b"), NULL));
+
+ // Absolute paths with a POSIX-style root
+ assert(view_equal(zix_path_root_path("/"), "/"));
+ assert(view_equal(zix_path_root_path("/."), "/"));
+ assert(view_equal(zix_path_root_path("/.."), "/"));
+ assert(view_equal(zix_path_root_path("//"), "/"));
+ assert(view_equal(zix_path_root_path("///a///"), "/"));
+ assert(view_equal(zix_path_root_path("///a///b"), "/"));
+ assert(view_equal(zix_path_root_path("/a"), "/"));
+ assert(view_equal(zix_path_root_path("/a/"), "/"));
+ assert(view_equal(zix_path_root_path("/a//b"), "/"));
+
+#ifdef _WIN32
+
+ // Paths like "C:/filename" have a drive letter
+ assert(view_equal(zix_path_root_path("C:"), "C:"));
+ assert(view_equal(zix_path_root_path("C:/"), "C:/"));
+ assert(view_equal(zix_path_root_path("C:/a"), "C:/"));
+ assert(view_equal(zix_path_root_path("C:/a/"), "C:/"));
+ assert(view_equal(zix_path_root_path("C:/a/b"), "C:/"));
+ assert(view_equal(zix_path_root_path("C:a"), "C:"));
+ assert(view_equal(zix_path_root_path("C:a/"), "C:"));
+
+ // Paths like "//host/" are network roots
+ assert(view_equal(zix_path_root_path("//host"), "//host"));
+ assert(view_equal(zix_path_root_path("//host/"), "//host/"));
+ assert(view_equal(zix_path_root_path("//host/a"), "//host/"));
+
+ // Backslash is a directory separator
+ assert(view_equal(zix_path_root_path("C:/a\\b"), "C:/"));
+ assert(view_equal(zix_path_root_path("C:\\"), "C:\\"));
+ assert(view_equal(zix_path_root_path("C:\\a"), "C:\\"));
+ assert(view_equal(zix_path_root_path("C:\\a/"), "C:\\"));
+ assert(view_equal(zix_path_root_path("C:\\a\\"), "C:\\"));
+ assert(view_equal(zix_path_root_path("C:a\\"), "C:"));
+
+#else
+
+ // "C:" is just a strange directory or file name prefix
+ assert(view_equal(zix_path_root_path("C:"), NULL));
+ assert(view_equal(zix_path_root_path("C:/"), NULL));
+ assert(view_equal(zix_path_root_path("C:/a"), NULL));
+ assert(view_equal(zix_path_root_path("C:/a/"), NULL));
+ assert(view_equal(zix_path_root_path("C:/a/b"), NULL));
+ assert(view_equal(zix_path_root_path("C:a"), NULL));
+ assert(view_equal(zix_path_root_path("C:a/"), NULL));
+
+ // Paths like "//host/" have redundant leading slashes
+ assert(view_equal(zix_path_root_path("//host"), "/"));
+ assert(view_equal(zix_path_root_path("//host/"), "/"));
+ assert(view_equal(zix_path_root_path("//host/a"), "/"));
+
+ // Backslash is a character in a directory or file name
+ assert(view_equal(zix_path_root_path("C:/a\\b"), NULL));
+ assert(view_equal(zix_path_root_path("C:\\"), NULL));
+ assert(view_equal(zix_path_root_path("C:\\a"), NULL));
+ assert(view_equal(zix_path_root_path("C:\\a/"), NULL));
+ assert(view_equal(zix_path_root_path("C:\\a\\"), NULL));
+ assert(view_equal(zix_path_root_path("C:a\\"), NULL));
+
+#endif
+}
+
+static void
+test_path_parent(void)
+{
+ // Absolute paths
+ assert(view_equal(zix_path_parent_path("/"), "/"));
+ assert(view_equal(zix_path_parent_path("/."), "/"));
+ assert(view_equal(zix_path_parent_path("/.."), "/"));
+ assert(view_equal(zix_path_parent_path("//"), "/"));
+ assert(view_equal(zix_path_parent_path("/a"), "/"));
+ assert(view_equal(zix_path_parent_path("/a/"), "/a"));
+ assert(view_equal(zix_path_parent_path("/a//b"), "/a"));
+
+ // Relative paths with no parent
+ assert(view_equal(zix_path_parent_path(""), NULL));
+ assert(view_equal(zix_path_parent_path("."), NULL));
+ assert(view_equal(zix_path_parent_path(".."), NULL));
+ assert(view_equal(zix_path_parent_path("NONDRIVE:"), NULL));
+ assert(view_equal(zix_path_parent_path("a"), NULL));
+
+ // Relative paths with a parent
+ assert(view_equal(zix_path_parent_path("../"), ".."));
+ assert(view_equal(zix_path_parent_path("./"), "."));
+ assert(view_equal(zix_path_parent_path("a/"), "a"));
+ assert(view_equal(zix_path_parent_path("a/b"), "a"));
+
+ // Superfluous leading and trailing separators
+ assert(view_equal(zix_path_parent_path("///a///"), "/a"));
+ assert(view_equal(zix_path_parent_path("///a///b"), "/a"));
+
+ // Relative paths with dot and dot-dot entries
+ assert(view_equal(zix_path_parent_path("a/."), "a"));
+ assert(view_equal(zix_path_parent_path("a/.."), "a"));
+ assert(view_equal(zix_path_parent_path("a/../"), "a/.."));
+ assert(view_equal(zix_path_parent_path("a/../b"), "a/.."));
+ assert(view_equal(zix_path_parent_path("a/./"), "a/."));
+ assert(view_equal(zix_path_parent_path("a/./b"), "a/."));
+
+#ifdef _WIN32
+
+ // Paths like "C:/filename" have a drive letter
+ assert(view_equal(zix_path_parent_path("C:"), "C:"));
+ assert(view_equal(zix_path_parent_path("C:/"), "C:/"));
+ assert(view_equal(zix_path_parent_path("C:/a"), "C:/"));
+ assert(view_equal(zix_path_parent_path("C:/a/"), "C:/a"));
+ assert(view_equal(zix_path_parent_path("C:/a/b"), "C:/a"));
+ assert(view_equal(zix_path_parent_path("C:/a\\b"), "C:/a"));
+ assert(view_equal(zix_path_parent_path("C:\\"), "C:\\"));
+ assert(view_equal(zix_path_parent_path("C:\\a"), "C:\\"));
+ assert(view_equal(zix_path_parent_path("C:\\a/"), "C:\\a"));
+ assert(view_equal(zix_path_parent_path("C:\\a\\"), "C:\\a"));
+ assert(view_equal(zix_path_parent_path("C:a"), "C:"));
+ assert(view_equal(zix_path_parent_path("C:a/"), "C:a"));
+ assert(view_equal(zix_path_parent_path("C:a\\"), "C:a"));
+
+ // Paths like "//host/" are network shares
+ assert(view_equal(zix_path_parent_path("//host"), "//host"));
+ assert(view_equal(zix_path_parent_path("//host/"), "//host/"));
+ assert(view_equal(zix_path_parent_path("//host/a"), "//host/"));
+
+#else
+
+ // "C:" is just a strange directory or file name prefix
+ assert(view_equal(zix_path_parent_path("C:"), NULL));
+ assert(view_equal(zix_path_parent_path("C:/"), "C:"));
+ assert(view_equal(zix_path_parent_path("C:/a"), "C:"));
+ assert(view_equal(zix_path_parent_path("C:/a/"), "C:/a"));
+ assert(view_equal(zix_path_parent_path("C:/a/b"), "C:/a"));
+ assert(view_equal(zix_path_parent_path("C:/a\\b"), "C:"));
+ assert(view_equal(zix_path_parent_path("C:\\"), NULL));
+ assert(view_equal(zix_path_parent_path("C:\\a"), NULL));
+ assert(view_equal(zix_path_parent_path("C:\\a/"), "C:\\a"));
+ assert(view_equal(zix_path_parent_path("C:\\a\\"), NULL));
+ assert(view_equal(zix_path_parent_path("C:a"), NULL));
+ assert(view_equal(zix_path_parent_path("C:a/"), "C:a"));
+ assert(view_equal(zix_path_parent_path("C:a\\"), NULL));
+
+ // Paths like "//host/" have redundant leading slashes
+ assert(view_equal(zix_path_parent_path("//host"), "/"));
+ assert(view_equal(zix_path_parent_path("//host/"), "/host"));
+ assert(view_equal(zix_path_parent_path("//host/a"), "/host"));
+
+#endif
+}
+
+static void
+test_path_filename(void)
+{
+ // Cases from <https://en.cppreference.com/w/cpp/filesystem/path/filename>
+ assert(view_equal(zix_path_filename("."), "."));
+ assert(view_equal(zix_path_filename(".."), ".."));
+ assert(view_equal(zix_path_filename("/"), NULL));
+ assert(view_equal(zix_path_filename("/foo/."), "."));
+ assert(view_equal(zix_path_filename("/foo/.."), ".."));
+ assert(view_equal(zix_path_filename("/foo/.bar"), ".bar"));
+ assert(view_equal(zix_path_filename("/foo/bar.txt"), "bar.txt"));
+ assert(view_equal(zix_path_filename("/foo/bar/"), NULL));
+
+ // Identities
+ assert(view_equal(zix_path_filename("."), "."));
+ assert(view_equal(zix_path_filename(".."), ".."));
+ assert(view_equal(zix_path_filename("a"), "a"));
+
+ // Absolute paths without filenames
+ assert(view_equal(zix_path_filename("/"), NULL));
+ assert(view_equal(zix_path_filename("//"), NULL));
+ assert(view_equal(zix_path_filename("///a///"), NULL));
+ assert(view_equal(zix_path_filename("/a/"), NULL));
+
+ // Absolute paths with filenames
+ assert(view_equal(zix_path_filename("///a///b"), "b"));
+ assert(view_equal(zix_path_filename("/a"), "a"));
+ assert(view_equal(zix_path_filename("/a//b"), "b"));
+
+ // Relative paths without filenames
+ assert(view_equal(zix_path_filename(""), NULL));
+ assert(view_equal(zix_path_filename("../"), NULL));
+ assert(view_equal(zix_path_filename("./"), NULL));
+ assert(view_equal(zix_path_filename("a/"), NULL));
+
+ // Relative paths with filenames
+ assert(view_equal(zix_path_filename("a/b"), "b"));
+
+ // Windows absolute network paths conveniently work generically
+ assert(view_equal(zix_path_filename("//host/"), NULL));
+ assert(view_equal(zix_path_filename("//host/a"), "a"));
+
+ // Paths with dot and dot-dot entries
+ assert(view_equal(zix_path_filename("/."), "."));
+ assert(view_equal(zix_path_filename("/.."), ".."));
+ assert(view_equal(zix_path_filename("a/."), "."));
+ assert(view_equal(zix_path_filename("a/.."), ".."));
+ assert(view_equal(zix_path_filename("a/../b"), "b"));
+ assert(view_equal(zix_path_filename("a/./b"), "b"));
+
+ // Paths with colons (maybe drive letters) that conveniently work generically
+ assert(view_equal(zix_path_filename("C:/"), NULL));
+ assert(view_equal(zix_path_filename("C:/a"), "a"));
+ assert(view_equal(zix_path_filename("C:/a/"), NULL));
+ assert(view_equal(zix_path_filename("C:/a/b"), "b"));
+ assert(view_equal(zix_path_filename("C:a/"), NULL));
+ assert(view_equal(zix_path_filename("NONDRIVE:"), "NONDRIVE:"));
+
+#ifdef _WIN32
+
+ // Relative paths can have a drive letter like "C:file"
+ assert(view_equal(zix_path_filename("C:"), NULL));
+ assert(view_equal(zix_path_filename("C:a"), "a"));
+ assert(view_equal(zix_path_filename("C:a\\"), NULL));
+
+ // Paths like "//host" are network roots
+ assert(view_equal(zix_path_filename("//host"), NULL));
+
+ // Backslash is a directory separator
+ assert(view_equal(zix_path_filename("C:/a\\b"), "b"));
+ assert(view_equal(zix_path_filename("C:\\"), NULL));
+ assert(view_equal(zix_path_filename("C:\\a"), "a"));
+ assert(view_equal(zix_path_filename("C:\\a/"), NULL));
+ assert(view_equal(zix_path_filename("C:\\a\\"), NULL));
+ assert(view_equal(zix_path_filename("C:\\a\\b"), "b"));
+ assert(view_equal(zix_path_filename("a\\b"), "b"));
+
+#else
+
+ // "C:" is just a strange directory or file name prefix
+ assert(view_equal(zix_path_filename("C:"), "C:"));
+ assert(view_equal(zix_path_filename("C:a"), "C:a"));
+ assert(view_equal(zix_path_filename("C:a\\"), "C:a\\"));
+
+ // Redundant reading slashes are ignored
+ assert(view_equal(zix_path_filename("//host"), "host"));
+
+ // Backslash is just a strange character in a directory or file name
+ assert(view_equal(zix_path_filename("C:/a\\b"), "a\\b"));
+ assert(view_equal(zix_path_filename("C:\\"), "C:\\"));
+ assert(view_equal(zix_path_filename("C:\\a"), "C:\\a"));
+ assert(view_equal(zix_path_filename("C:\\a/"), NULL));
+ assert(view_equal(zix_path_filename("C:\\a\\"), "C:\\a\\"));
+ assert(view_equal(zix_path_filename("C:\\a\\b"), "C:\\a\\b"));
+ assert(view_equal(zix_path_filename("a\\b"), "a\\b"));
+
+#endif
+}
+
+static void
+test_path_stem(void)
+{
+ assert(view_equal(zix_path_stem(""), NULL));
+ assert(view_equal(zix_path_stem("."), "."));
+ assert(view_equal(zix_path_stem(".."), ".."));
+ assert(view_equal(zix_path_stem(".a"), ".a"));
+ assert(view_equal(zix_path_stem(".hidden"), ".hidden"));
+ assert(view_equal(zix_path_stem(".hidden.txt"), ".hidden"));
+ assert(view_equal(zix_path_stem("/"), NULL));
+ assert(view_equal(zix_path_stem("//host/name"), "name"));
+ assert(view_equal(zix_path_stem("/a."), "a"));
+ assert(view_equal(zix_path_stem("/a.txt"), "a"));
+ assert(view_equal(zix_path_stem("/a/."), "."));
+ assert(view_equal(zix_path_stem("/a/.."), ".."));
+ assert(view_equal(zix_path_stem("/a/.hidden"), ".hidden"));
+ assert(view_equal(zix_path_stem("/a/b."), "b"));
+ assert(view_equal(zix_path_stem("/a/b.tar.gz"), "b.tar"));
+ assert(view_equal(zix_path_stem("/a/b.txt"), "b"));
+ assert(view_equal(zix_path_stem("/a/b/.hidden"), ".hidden"));
+ assert(view_equal(zix_path_stem("C:/name"), "name"));
+ assert(view_equal(zix_path_stem("C:dir/name"), "name"));
+ assert(view_equal(zix_path_stem("a"), "a"));
+ assert(view_equal(zix_path_stem("a."), "a"));
+ assert(view_equal(zix_path_stem("a..txt"), "a."));
+ assert(view_equal(zix_path_stem("a.txt"), "a"));
+ assert(view_equal(zix_path_stem("a/."), "."));
+ assert(view_equal(zix_path_stem("a/.."), ".."));
+ assert(view_equal(zix_path_stem("a/b."), "b"));
+ assert(view_equal(zix_path_stem("a/b.tar.gz"), "b.tar"));
+ assert(view_equal(zix_path_stem("a/b.txt"), "b"));
+
+#ifdef _WIN32
+ assert(view_equal(zix_path_stem("C:."), "."));
+ assert(view_equal(zix_path_stem("C:.a"), ".a"));
+ assert(view_equal(zix_path_stem("C:a"), "a"));
+#else
+ assert(view_equal(zix_path_stem("C:."), "C:"));
+ assert(view_equal(zix_path_stem("C:.a"), "C:"));
+ assert(view_equal(zix_path_stem("C:a"), "C:a"));
+#endif
+}
+
+static void
+test_path_extension(void)
+{
+ assert(view_equal(zix_path_extension(""), NULL));
+ assert(view_equal(zix_path_extension("."), NULL));
+ assert(view_equal(zix_path_extension(".."), NULL));
+ assert(view_equal(zix_path_extension(".a"), NULL));
+ assert(view_equal(zix_path_extension(".hidden"), NULL));
+ assert(view_equal(zix_path_extension(".hidden.txt"), ".txt"));
+ assert(view_equal(zix_path_extension("/"), NULL));
+ assert(view_equal(zix_path_extension("/a."), "."));
+ assert(view_equal(zix_path_extension("/a.txt"), ".txt"));
+ assert(view_equal(zix_path_extension("/a/."), NULL));
+ assert(view_equal(zix_path_extension("/a/.."), NULL));
+ assert(view_equal(zix_path_extension("/a/.hidden"), NULL));
+ assert(view_equal(zix_path_extension("/a/b."), "."));
+ assert(view_equal(zix_path_extension("/a/b.tar.gz"), ".gz"));
+ assert(view_equal(zix_path_extension("/a/b.txt"), ".txt"));
+ assert(view_equal(zix_path_extension("/a/b/.hidden"), NULL));
+ assert(view_equal(zix_path_extension("C:/.hidden.txt"), ".txt"));
+ assert(view_equal(zix_path_extension("C:a.txt"), ".txt"));
+ assert(view_equal(zix_path_extension("a"), NULL));
+ assert(view_equal(zix_path_extension("a."), "."));
+ assert(view_equal(zix_path_extension("a..txt"), ".txt"));
+ assert(view_equal(zix_path_extension("a.tar.gz"), ".gz"));
+ assert(view_equal(zix_path_extension("a/."), NULL));
+ assert(view_equal(zix_path_extension("a/.."), NULL));
+ assert(view_equal(zix_path_extension("a/b."), "."));
+ assert(view_equal(zix_path_extension("a/b.tar.gz"), ".gz"));
+ assert(view_equal(zix_path_extension("a/b.txt"), ".txt"));
+
+#ifdef _WIN32
+ assert(view_equal(zix_path_extension("C:."), NULL));
+ assert(view_equal(zix_path_extension("C:/.hidden"), NULL));
+ assert(view_equal(zix_path_extension("C:/a.txt"), ".txt"));
+#else
+ assert(view_equal(zix_path_extension("C:."), "."));
+ assert(view_equal(zix_path_extension("C:/.hidden"), NULL));
+ assert(view_equal(zix_path_extension("C:/a.txt"), ".txt"));
+#endif
+}
+
+static void
+test_path_is_absolute(void)
+{
+ assert(!zix_path_is_absolute("."));
+ assert(!zix_path_is_absolute(".."));
+ assert(!zix_path_is_absolute("../"));
+ assert(!zix_path_is_absolute("../a"));
+ assert(!zix_path_is_absolute("../a/"));
+ assert(!zix_path_is_absolute("a"));
+ assert(!zix_path_is_absolute("a/b"));
+ assert(!zix_path_is_relative("//host/a"));
+ assert(zix_path_is_absolute("//host/a"));
+ assert(zix_path_is_relative("."));
+ assert(zix_path_is_relative(".."));
+ assert(zix_path_is_relative("../"));
+ assert(zix_path_is_relative("../a"));
+ assert(zix_path_is_relative("../a/"));
+ assert(zix_path_is_relative("a"));
+ assert(zix_path_is_relative("a/b"));
+
+#ifdef _WIN32
+ // Paths starting with root names are absolute
+ assert(!zix_path_is_absolute("/"));
+ assert(!zix_path_is_absolute("/a"));
+ assert(!zix_path_is_absolute("/a/b"));
+ assert(!zix_path_is_relative("C:/a/b"));
+ assert(!zix_path_is_relative("C:\\a\\b"));
+ assert(!zix_path_is_relative("D:/a/b"));
+ assert(!zix_path_is_relative("D:\\a\\b"));
+ assert(zix_path_is_absolute("C:/a/b"));
+ assert(zix_path_is_absolute("C:\\a\\b"));
+ assert(zix_path_is_absolute("D:/a/b"));
+ assert(zix_path_is_absolute("D:\\a\\b"));
+ assert(zix_path_is_relative("/"));
+ assert(zix_path_is_relative("/a"));
+ assert(zix_path_is_relative("/a/b"));
+#else
+ // Paths starting with slashes are absolute
+ assert(!zix_path_is_absolute("C:/a/b"));
+ assert(!zix_path_is_absolute("C:\\a\\b"));
+ assert(!zix_path_is_absolute("D:/a/b"));
+ assert(!zix_path_is_absolute("D:\\a\\b"));
+ assert(!zix_path_is_relative("/"));
+ assert(!zix_path_is_relative("/a"));
+ assert(!zix_path_is_relative("/a/b"));
+ assert(zix_path_is_absolute("/"));
+ assert(zix_path_is_absolute("/a"));
+ assert(zix_path_is_absolute("/a/b"));
+ assert(zix_path_is_relative("C:/a/b"));
+ assert(zix_path_is_relative("C:\\a\\b"));
+ assert(zix_path_is_relative("D:/a/b"));
+ assert(zix_path_is_relative("D:\\a\\b"));
+
+#endif
+}
+
+int
+main(void)
+{
+ test_path_root_name();
+ test_path_root();
+ test_path_parent();
+ test_path_filename();
+ test_path_stem();
+ test_path_extension();
+ test_path_is_absolute();
+
+ test_path_join();
+ test_path_preferred();
+ test_path_lexically_normal();
+ test_path_lexically_relative();
+
+ return 0;
+}