diff options
author | David Robillard <d@drobilla.net> | 2022-10-23 13:41:15 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2022-10-23 13:46:58 -0400 |
commit | c886d489576cd0bc33d7d22d81981c794067946f (patch) | |
tree | f43c8d872401ed80b37f974516d9e9f2fee9d765 | |
parent | f95a698b94069ed03f6a8f2d0f7eb089d66c91ef (diff) | |
download | zix-c886d489576cd0bc33d7d22d81981c794067946f.tar.gz zix-c886d489576cd0bc33d7d22d81981c794067946f.tar.bz2 zix-c886d489576cd0bc33d7d22d81981c794067946f.zip |
Add path API
-rw-r--r-- | .clang-format | 1 | ||||
-rw-r--r-- | .gitlab-ci.yml | 16 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | include/zix/function_types.h | 2 | ||||
-rw-r--r-- | include/zix/path.h | 246 | ||||
-rw-r--r-- | include/zix/zix.h | 8 | ||||
-rw-r--r-- | meson.build | 20 | ||||
-rw-r--r-- | meson_options.txt | 3 | ||||
-rw-r--r-- | src/index_range.h | 28 | ||||
-rw-r--r-- | src/path.c | 712 | ||||
-rw-r--r-- | src/path_iter.h | 31 | ||||
-rw-r--r-- | test/cpp/.clang-tidy | 13 | ||||
-rw-r--r-- | test/cpp/test_path_std.cpp | 496 | ||||
-rw-r--r-- | test/headers/test_headers.c | 1 | ||||
-rw-r--r-- | test/test_path.c | 948 |
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: @@ -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; +} |