diff --git a/src/libutil-tests/unix/file-descriptor.cc b/src/libutil-tests/unix/file-descriptor.cc index e1ccca3f2..185b50d93 100644 --- a/src/libutil-tests/unix/file-descriptor.cc +++ b/src/libutil-tests/unix/file-descriptor.cc @@ -1,4 +1,5 @@ #include +#include #include "nix/util/file-descriptor.hh" #include "nix/util/file-system.hh" @@ -194,4 +195,57 @@ TEST(fchmodatTryNoFollow, fallbackWithoutProc) } #endif +/* ---------------------------------------------------------------------------- + * readLinkAt + * --------------------------------------------------------------------------*/ + +TEST(readLinkAt, works) +{ + std::filesystem::path tmpDir = nix::createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true); + + std::string mediumTarget(PATH_MAX / 2, 'x'); + std::string longTarget(PATH_MAX - 1, 'y'); + + { + RestoreSink sink(/*startFsync=*/false); + sink.dstPath = tmpDir; + sink.dirFd = openDirectory(tmpDir); + sink.createSymlink(CanonPath("link"), "target"); + sink.createSymlink(CanonPath("relative"), "../relative/path"); + sink.createSymlink(CanonPath("absolute"), "/absolute/path"); + sink.createSymlink(CanonPath("medium"), mediumTarget); + sink.createSymlink(CanonPath("long"), longTarget); + sink.createDirectory(CanonPath("a")); + sink.createDirectory(CanonPath("a/b")); + sink.createSymlink(CanonPath("a/b/link"), "nested_target"); + sink.createRegularFile(CanonPath("regular"), [](CreateRegularFileSink &) {}); + sink.createDirectory(CanonPath("dir")); + } + + AutoCloseFD dirFd = openDirectory(tmpDir); + + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("link")), "target"); + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("relative")), "../relative/path"); + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("absolute")), "/absolute/path"); + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("medium")), mediumTarget); + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("long")), longTarget); + EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("a/b/link")), "nested_target"); + + AutoCloseFD subDirFd = openDirectory(tmpDir / "a"); + EXPECT_EQ(readLinkAt(subDirFd.get(), CanonPath("b/link")), "nested_target"); + + EXPECT_THAT( + [&] { readLinkAt(dirFd.get(), CanonPath("regular")); }, + Throws(::testing::Field(&SysError::errNo, EINVAL))); + + EXPECT_THAT( + [&] { readLinkAt(dirFd.get(), CanonPath("dir")); }, + Throws(::testing::Field(&SysError::errNo, EINVAL))); + + EXPECT_THAT( + [&] { readLinkAt(dirFd.get(), CanonPath("nonexistent")); }, + Throws(::testing::Field(&SysError::errNo, ENOENT))); +} + } // namespace nix diff --git a/src/libutil/include/nix/util/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh index cfb0fb8ee..20649ef10 100644 --- a/src/libutil/include/nix/util/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -268,6 +268,11 @@ Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & p */ void fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t mode); +/* + * Read a symlink relative to a directory file descriptor. + */ +std::string readLinkAt(Descriptor dirFd, const CanonPath & path); + } // namespace unix #endif diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc index 99ff9011d..ca1ef1dbc 100644 --- a/src/libutil/unix/file-descriptor.cc +++ b/src/libutil/unix/file-descriptor.cc @@ -411,4 +411,20 @@ Descriptor unix::openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPa return openFileEnsureBeneathNoSymlinksIterative(dirFd, path, flags, mode); } +std::string unix::readLinkAt(Descriptor dirFd, const CanonPath & path) +{ + assert(!path.isRoot()); + assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */ + std::vector buf; + for (ssize_t bufSize = PATH_MAX / 4; true; bufSize += bufSize / 2) { + checkInterrupt(); + buf.resize(bufSize); + ssize_t rlSize = ::readlinkat(dirFd, path.rel_c_str(), buf.data(), bufSize); + if (rlSize == -1) + throw SysError("reading symbolic link '%1%'", path); + else if (rlSize < bufSize) + return {buf.data(), static_cast(rlSize)}; + } +} + } // namespace nix