libutil: Add unix::readLinkAt function

This will be used in a following commit.
This commit is contained in:
Sergei Zimmerman
2025-12-23 03:11:27 +03:00
parent d27e4ed963
commit 3798eb8efd
3 changed files with 75 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#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<SysError>(::testing::Field(&SysError::errNo, EINVAL)));
EXPECT_THAT(
[&] { readLinkAt(dirFd.get(), CanonPath("dir")); },
Throws<SysError>(::testing::Field(&SysError::errNo, EINVAL)));
EXPECT_THAT(
[&] { readLinkAt(dirFd.get(), CanonPath("nonexistent")); },
Throws<SysError>(::testing::Field(&SysError::errNo, ENOENT)));
}
} // namespace nix

View File

@@ -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

View File

@@ -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<char> 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<std::size_t>(rlSize)};
}
}
} // namespace nix