Skip to content

Commit

Permalink
Support native Windows paths
Browse files Browse the repository at this point in the history
This leverages the previous commit to make `absPath` and `canonPath`
work on Windows without messing up `CanonPath`.
  • Loading branch information
Ericson2314 committed Jan 29, 2024
1 parent eb5faab commit c92b577
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 12 deletions.
61 changes: 61 additions & 0 deletions src/libutil/file-path-impl.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<class CharT0>
struct WindowsPath
{
using CharT = CharT0;

using String = std::basic_string<CharT>;

using StringView = std::basic_string_view<CharT>;

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<char>
#else
UnixPath
#endif
;


/**
* Core pure path canonicalization algorithm.
*
Expand Down
28 changes: 27 additions & 1 deletion src/libutil/file-system.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@
#include <sys/time.h>
#include <unistd.h>

#ifdef _WIN32
# include <shlwapi.h>
#endif

namespace fs = std::filesystem;

namespace nix {


static inline bool isAbsolute(PathView path)
{
#ifndef _WIN32
return path[0] == '/';
#else
return !PathIsRelativeA(std::string { path }.c_str());
#endif
}


Expand Down Expand Up @@ -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<PathView> drive;

// UNC paths do, in fact, start with a path separater.
if (!WindowsPath<char>::isPathSep(path[0])) {
auto nextPathSep = WindowsPath<char>::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<UnixPath>(
auto ret = canonPathInner<NativePath>(
path,
[&followCount, &temp, maxFollow, resolveSymlinks]
(std::string & s, std::string_view & path) {
Expand All @@ -94,6 +114,12 @@ Path canonPath(PathView path, bool resolveSymlinks)
}
}
});

#ifdef _WIN32
if (drive)
ret = std::string { *drive } + std::move(ret);
#endif
return ret;
}


Expand Down
3 changes: 3 additions & 0 deletions src/libutil/local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
30 changes: 19 additions & 11 deletions tests/unit/libutil/tests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

#include <numeric>

#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 ------------------------------------------------*/
Expand All @@ -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);
}


Expand Down Expand Up @@ -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);
}

Expand All @@ -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) {
Expand Down Expand Up @@ -197,7 +205,7 @@ namespace nix {
* --------------------------------------------------------------------------*/

TEST(pathExists, rootExists) {
ASSERT_TRUE(pathExists("/"));
ASSERT_TRUE(pathExists(FS_ROOT));
}

TEST(pathExists, cwdExists) {
Expand Down

0 comments on commit c92b577

Please sign in to comment.