From c92b577db95788b9ab8ea93acba443f9b6d20643 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 14 Jan 2024 14:30:25 -0500 Subject: [PATCH] Support native Windows paths This leverages the previous commit to make `absPath` and `canonPath` work on Windows without messing up `CanonPath`. --- src/libutil/file-path-impl.hh | 61 +++++++++++++++++++++++++++++++++++ src/libutil/file-system.cc | 28 +++++++++++++++- src/libutil/local.mk | 3 ++ tests/unit/libutil/tests.cc | 30 ++++++++++------- 4 files changed, 110 insertions(+), 12 deletions(-) diff --git a/src/libutil/file-path-impl.hh b/src/libutil/file-path-impl.hh index c47dd94c9062..8d886786ce01 100644 --- a/src/libutil/file-path-impl.hh +++ b/src/libutil/file-path-impl.hh @@ -43,6 +43,67 @@ struct UnixPath }; +/** + * Windows-style path primitives. + * + * The character type is a parameter because while windows paths rightly + * work over UTF-16 (*) using `wchar_t`, at the current time we are + * often manipulating them converted to UTF-8 (*) using `char`. + * + * (Actually neither are guaranteed to be valid unicode; both are + * arbitrary non-0 8- or 16-bit bytes. But for charcters with specifical + * meaning like '/', '\\', ':', etc., we refer to an encoding scheme, + * and also for sake of UIs that display paths a text.) + */ +template +struct WindowsPath +{ + using CharT = CharT0; + + using String = std::basic_string; + + using StringView = std::basic_string_view; + + constexpr static CharT preferredSep = '\\'; + + static inline bool isPathSep(CharT c) + { + return c == '/' || c == preferredSep; + } + + static size_t findPathSep(StringView path, size_t from = 0) + { + size_t p1 = path.find('/', from); + size_t p2 = path.find(preferredSep, from); + return p1 == String::npos ? p2 : + p2 == String::npos ? p1 : + std::min(p1, p2); + } + + static size_t rfindPathSep(StringView path, size_t from = String::npos) + { + size_t p1 = path.rfind('/', from); + size_t p2 = path.rfind(preferredSep, from); + return p1 == String::npos ? p2 : + p2 == String::npos ? p1 : + std::max(p1, p2); + } +}; + + +/** + * @todo Revisit choice of `char` or `wchar_t` for `WindowsPath` + * argument. + */ +using NativePath = +#ifdef _WIN32 + WindowsPath +#else + UnixPath +#endif + ; + + /** * Core pure path canonicalization algorithm. * diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index e75b833b1379..b930cc9cbf0f 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -18,6 +18,10 @@ #include #include +#ifdef _WIN32 +# include +#endif + namespace fs = std::filesystem; namespace nix { @@ -25,7 +29,11 @@ namespace nix { static inline bool isAbsolute(PathView path) { +#ifndef _WIN32 return path[0] == '/'; +#else + return !PathIsRelativeA(std::string { path }.c_str()); +#endif } @@ -69,13 +77,25 @@ Path canonPath(PathView path, bool resolveSymlinks) if (!isAbsolute(path)) throw Error("not an absolute path: '%1%'", path); +#ifdef _WIN32 + std::optional drive; + + // UNC paths do, in fact, start with a path separater. + if (!WindowsPath::isPathSep(path[0])) { + auto nextPathSep = WindowsPath::findPathSep(path); + assert(nextPathSep != path.npos); + drive = path.substr(0, nextPathSep); + path = path.substr(nextPathSep); + } +#endif + std::string temp; /* Count the number of times we follow a symlink and stop at some arbitrary (but high) limit to prevent infinite loops. */ unsigned int followCount = 0, maxFollow = 1024; - return canonPathInner( + auto ret = canonPathInner( path, [&followCount, &temp, maxFollow, resolveSymlinks] (std::string & s, std::string_view & path) { @@ -94,6 +114,12 @@ Path canonPath(PathView path, bool resolveSymlinks) } } }); + +#ifdef _WIN32 + if (drive) + ret = std::string { *drive } + std::move(ret); +#endif + return ret; } diff --git a/src/libutil/local.mk b/src/libutil/local.mk index 200026c1e06f..12c22ef53900 100644 --- a/src/libutil/local.mk +++ b/src/libutil/local.mk @@ -9,6 +9,9 @@ libutil_SOURCES := $(wildcard $(d)/*.cc $(d)/signature/*.cc) libutil_CXXFLAGS += -I src/libutil libutil_LDFLAGS += $(THREAD_LDFLAGS) $(LIBCURL_LIBS) $(SODIUM_LIBS) $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context +ifdef HOST_WINDOWS + libutil_LDFLAGS += -lshlwapi +endif $(foreach i, $(wildcard $(d)/args/*.hh), \ $(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644))) diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index 568f03f702d4..4406fd184550 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -9,6 +9,14 @@ #include +#ifdef _WIN32 +# define FS_SEP "\\" +# define FS_ROOT "C:" FS_SEP // Need a mounted one, C drive is likely +#else +# define FS_SEP "/" +# define FS_ROOT FS_SEP +#endif + namespace nix { /* ----------- tests for util.hh ------------------------------------------------*/ @@ -18,9 +26,9 @@ namespace nix { * --------------------------------------------------------------------------*/ TEST(absPath, doesntChangeRoot) { - auto p = absPath("/"); + auto p = absPath(FS_ROOT); - ASSERT_EQ(p, "/"); + ASSERT_EQ(p, FS_ROOT); } @@ -53,11 +61,11 @@ namespace nix { TEST(absPath, pathIsCanonicalised) { - auto path = "/some/path/with/trailing/dot/."; + auto path = FS_ROOT "some/path/with/trailing/dot/."; auto p1 = absPath(path); auto p2 = absPath(p1); - ASSERT_EQ(p1, "/some/path/with/trailing/dot"); + ASSERT_EQ(p1, FS_ROOT "some" FS_SEP "path" FS_SEP "with" FS_SEP "trailing" FS_SEP "dot"); ASSERT_EQ(p1, p2); } @@ -66,24 +74,24 @@ namespace nix { * --------------------------------------------------------------------------*/ TEST(canonPath, removesTrailingSlashes) { - auto path = "/this/is/a/path//"; + auto path = FS_ROOT "this/is/a/path//"; auto p = canonPath(path); - ASSERT_EQ(p, "/this/is/a/path"); + ASSERT_EQ(p, FS_ROOT "this" FS_SEP "is" FS_SEP "a" FS_SEP "path"); } TEST(canonPath, removesDots) { - auto path = "/this/./is/a/path/./"; + auto path = FS_ROOT "this/./is/a/path/./"; auto p = canonPath(path); - ASSERT_EQ(p, "/this/is/a/path"); + ASSERT_EQ(p, FS_ROOT "this" FS_SEP "is" FS_SEP "a" FS_SEP "path"); } TEST(canonPath, removesDots2) { - auto path = "/this/a/../is/a////path/foo/.."; + auto path = FS_ROOT "this/a/../is/a////path/foo/.."; auto p = canonPath(path); - ASSERT_EQ(p, "/this/is/a/path"); + ASSERT_EQ(p, FS_ROOT "this" FS_SEP "is" FS_SEP "a" FS_SEP "path"); } TEST(canonPath, requiresAbsolutePath) { @@ -197,7 +205,7 @@ namespace nix { * --------------------------------------------------------------------------*/ TEST(pathExists, rootExists) { - ASSERT_TRUE(pathExists("/")); + ASSERT_TRUE(pathExists(FS_ROOT)); } TEST(pathExists, cwdExists) {