diff --git a/src/libutil-tests/meson.build b/src/libutil-tests/meson.build index 019bdb6d2..24cbf18e5 100644 --- a/src/libutil-tests/meson.build +++ b/src/libutil-tests/meson.build @@ -33,6 +33,9 @@ deps_private += rapidcheck gtest = dependency('gtest', main : true) deps_private += gtest +gmock = dependency('gmock') +deps_private += gmock + configdata = configuration_data() configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) @@ -72,6 +75,7 @@ sources = files( 'position.cc', 'processes.cc', 'sort.cc', + 'source-accessor.cc', 'spawn.cc', 'strings.cc', 'suggestions.cc', diff --git a/src/libutil-tests/source-accessor.cc b/src/libutil-tests/source-accessor.cc new file mode 100644 index 000000000..a6f77d42e --- /dev/null +++ b/src/libutil-tests/source-accessor.cc @@ -0,0 +1,138 @@ +#include "nix/util/fs-sink.hh" +#include "nix/util/file-system.hh" +#include "nix/util/processes.hh" + +#include +#include +#include + +namespace nix { + +MATCHER_P2(HasContents, path, expected, "") +{ + auto stat = arg->maybeLstat(path); + if (!stat) { + *result_listener << arg->showPath(path) << " does not exist"; + return false; + } + if (stat->type != SourceAccessor::tRegular) { + *result_listener << arg->showPath(path) << " is not a regular file"; + return false; + } + auto actual = arg->readFile(path); + if (actual != expected) { + *result_listener << arg->showPath(path) << " has contents " << ::testing::PrintToString(actual); + return false; + } + return true; +} + +MATCHER_P2(HasSymlink, path, target, "") +{ + auto stat = arg->maybeLstat(path); + if (!stat) { + *result_listener << arg->showPath(path) << " does not exist"; + return false; + } + if (stat->type != SourceAccessor::tSymlink) { + *result_listener << arg->showPath(path) << " is not a symlink"; + return false; + } + auto actual = arg->readLink(path); + if (actual != target) { + *result_listener << arg->showPath(path) << " points to " << ::testing::PrintToString(actual); + return false; + } + return true; +} + +MATCHER_P2(HasDirectory, path, dirents, "") +{ + auto stat = arg->maybeLstat(path); + if (!stat) { + *result_listener << arg->showPath(path) << " does not exist"; + return false; + } + if (stat->type != SourceAccessor::tDirectory) { + *result_listener << arg->showPath(path) << " is not a directory"; + return false; + } + auto actual = arg->readDirectory(path); + std::set actualKeys, expectedKeys(dirents.begin(), dirents.end()); + for (auto & [k, _] : actual) + actualKeys.insert(k); + if (actualKeys != expectedKeys) { + *result_listener << arg->showPath(path) << " has entries " << ::testing::PrintToString(actualKeys); + return false; + } + return true; +} + +class FSSourceAccessorTest : public ::testing::Test +{ +protected: + std::filesystem::path tmpDir; + std::unique_ptr delTmpDir; + + void SetUp() override + { + tmpDir = nix::createTempDir(); + delTmpDir = std::make_unique(tmpDir, true); + } + + void TearDown() override + { + delTmpDir.reset(); + } +}; + +TEST_F(FSSourceAccessorTest, works) +{ + { + RestoreSink sink(false); + sink.dstPath = tmpDir; + sink.dirFd = openDirectory(tmpDir); + sink.createDirectory(CanonPath("subdir")); + sink.createRegularFile(CanonPath("file1"), [](CreateRegularFileSink & crf) { crf("content1"); }); + sink.createRegularFile(CanonPath("subdir/file2"), [](CreateRegularFileSink & crf) { crf("content2"); }); + sink.createSymlink(CanonPath("rootlink"), "target"); + sink.createDirectory(CanonPath("a")); + sink.createSymlink(CanonPath("a/dirlink"), "../subdir"); + } + + EXPECT_THAT(makeFSSourceAccessor(tmpDir / "file1"), HasContents(CanonPath::root, "content1")); + EXPECT_THAT(makeFSSourceAccessor(tmpDir / "rootlink"), HasSymlink(CanonPath::root, "target")); + EXPECT_THAT( + makeFSSourceAccessor(tmpDir), + HasDirectory(CanonPath::root, std::set{"file1", "subdir", "rootlink", "a"})); + EXPECT_THAT(makeFSSourceAccessor(tmpDir / "subdir"), HasDirectory(CanonPath::root, std::set{"file2"})); + + { + auto accessor = makeFSSourceAccessor(tmpDir); + EXPECT_THAT(accessor, HasContents(CanonPath("file1"), "content1")); + EXPECT_THAT(accessor, HasContents(CanonPath("subdir/file2"), "content2")); + + EXPECT_TRUE(accessor->pathExists(CanonPath("file1"))); + EXPECT_FALSE(accessor->pathExists(CanonPath("nonexistent"))); + + EXPECT_THROW(accessor->readFile(CanonPath("a/dirlink/file2")), SymlinkNotAllowed); + EXPECT_THROW(accessor->maybeLstat(CanonPath("a/dirlink/file2")), SymlinkNotAllowed); + EXPECT_THROW(accessor->readDirectory(CanonPath("a/dirlink")), SymlinkNotAllowed); + EXPECT_THROW(accessor->pathExists(CanonPath("a/dirlink/file2")), SymlinkNotAllowed); + } + + { + auto accessor = makeFSSourceAccessor(tmpDir / "nonexistent"); + EXPECT_FALSE(accessor->maybeLstat(CanonPath::root)); + EXPECT_THROW(accessor->readFile(CanonPath::root), SystemError); + } + + { + auto accessor = makeFSSourceAccessor(tmpDir, true); + EXPECT_EQ(accessor->getLastModified(), 0); + accessor->maybeLstat(CanonPath("file1")); + EXPECT_GT(accessor->getLastModified(), 0); + } +} + +} // namespace nix diff --git a/src/libutil/include/nix/util/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh index d04984588..441ec4d4f 100644 --- a/src/libutil/include/nix/util/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -2,7 +2,6 @@ ///@file #include "nix/util/canon-path.hh" -#include "nix/util/types.hh" #include "nix/util/error.hh" #ifdef _WIN32 @@ -236,18 +235,6 @@ std::wstring handleToFileName(Descriptor handle); #ifndef _WIN32 namespace unix { -struct SymlinkNotAllowed : public Error -{ - CanonPath path; - - SymlinkNotAllowed(CanonPath path) - /* Can't provide better error message, since the parent directory is only known to the caller. */ - : Error("relative path '%s' points to a symlink, which is not allowed", path.rel()) - , path(std::move(path)) - { - } -}; - /** * Safe(r) function to open \param path file relative to \param dirFd, while * disallowing escaping from a directory and resolving any symlinks in the diff --git a/src/libutil/include/nix/util/source-accessor.hh b/src/libutil/include/nix/util/source-accessor.hh index 1006895b3..59af81569 100644 --- a/src/libutil/include/nix/util/source-accessor.hh +++ b/src/libutil/include/nix/util/source-accessor.hh @@ -222,6 +222,24 @@ ref makeEmptySourceAccessor(); */ MakeError(RestrictedPathError, Error); +struct SymlinkNotAllowed : public Error +{ + CanonPath path; + + SymlinkNotAllowed(CanonPath path) + : Error("relative path '%s' points to a symlink, which is not allowed", path.rel()) + , path(std::move(path)) + { + } + + template + SymlinkNotAllowed(CanonPath path, const std::string & fs, Args &&... args) + : Error(fs, std::forward(args)...) + , path(std::move(path)) + { + } +}; + /** * Return an accessor for the root filesystem. */ @@ -233,7 +251,7 @@ ref getFSSourceAccessor(); * elements, and that absolute symlinks are resolved relative to * `root`. */ -ref makeFSSourceAccessor(std::filesystem::path root); +ref makeFSSourceAccessor(std::filesystem::path root, bool trackLastModified = false); /** * Construct an accessor that presents a "union" view of a vector of diff --git a/src/libutil/posix-source-accessor.cc b/src/libutil/posix-source-accessor.cc index abbab45db..4d56f9af2 100644 --- a/src/libutil/posix-source-accessor.cc +++ b/src/libutil/posix-source-accessor.cc @@ -208,7 +208,7 @@ void PosixSourceAccessor::assertNoSymlinks(CanonPath path) while (!path.isRoot()) { auto st = cachedLstat(path); if (st && S_ISLNK(st->st_mode)) - throw Error("path '%s' is a symlink", showPath(path)); + throw SymlinkNotAllowed(path, "path '%s' is a symlink", showPath(path)); path.pop(); } } @@ -219,8 +219,8 @@ ref getFSSourceAccessor() return rootFS; } -ref makeFSSourceAccessor(std::filesystem::path root) +ref makeFSSourceAccessor(std::filesystem::path root, bool trackLastModified) { - return make_ref(std::move(root)); + return make_ref(std::move(root), trackLastModified); } } // namespace nix diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc index d90342ff0..bdb8054eb 100644 --- a/src/libutil/unix/file-descriptor.cc +++ b/src/libutil/unix/file-descriptor.cc @@ -3,6 +3,7 @@ #include "nix/util/signals.hh" #include "nix/util/finally.hh" #include "nix/util/serialise.hh" +#include "nix/util/source-accessor.hh" #include #include @@ -301,10 +302,10 @@ openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & pat if (errno == ENOTDIR) /* Path component might be a symlink. */ { struct ::stat st; if (::fstatat(getParentFd(), component.c_str(), &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(st.st_mode)) - throw unix::SymlinkNotAllowed(path2); + throw SymlinkNotAllowed(path2); errno = ENOTDIR; /* Restore the errno. */ } else if (errno == ELOOP) { - throw unix::SymlinkNotAllowed(path2); + throw SymlinkNotAllowed(path2); } return INVALID_DESCRIPTOR; @@ -315,7 +316,7 @@ openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & pat auto res = ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags | O_NOFOLLOW, mode); if (res < 0 && errno == ELOOP) - throw unix::SymlinkNotAllowed(path); + throw SymlinkNotAllowed(path); return res; } @@ -328,7 +329,7 @@ Descriptor unix::openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPa dirFd, path.rel_c_str(), flags, static_cast(mode), RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS); if (maybeFd) { if (*maybeFd < 0 && errno == ELOOP) - throw unix::SymlinkNotAllowed(path); + throw SymlinkNotAllowed(path); return *maybeFd; } #endif