From 31f976dc198136f6a57d1d5ddce5ee045e5bbb71 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 26 Mar 2025 01:04:12 +0100 Subject: [PATCH] libutil: guess or invent a path from file descriptors This is useful for certain error recovery paths (no pun intended) that does not thread through the original path name. Change-Id: I2d800740cb4f9912e64c923120d3f977c58ccb7e Signed-off-by: Raito Bezarius --- lix/libutil/file-descriptor.cc | 23 ++++++++++ lix/libutil/file-descriptor.hh | 18 ++++++++ meson.build | 5 +++ tests/unit/libutil/tests.cc | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/lix/libutil/file-descriptor.cc b/lix/libutil/file-descriptor.cc index 25b4feeff..826ad6182 100644 --- a/lix/libutil/file-descriptor.cc +++ b/lix/libutil/file-descriptor.cc @@ -154,6 +154,29 @@ int AutoCloseFD::get() const return fd; } +std::string guessOrInventPathFromFD(int fd) +{ + assert(fd >= 0); + /* On Linux, there's no F_GETPATH available. + * But we can read /proc/ */ +#if __linux__ + try { + return readLink(fmt("/proc/self/fd/%1%", fd).c_str()); + } catch (...) { + } +#elif defined (HAVE_F_GETPATH) && HAVE_F_GETPATH + std::string fdName(PATH_MAX, '\0'); + if (fcntl(fd, F_GETPATH, fdName.data()) != -1) { + fdName.resize(strlen(fdName.c_str())); + return fdName; + } +#else +#error "No implementation for retrieving file descriptors path." +#endif + + return fmt("", fd); +} + void AutoCloseFD::close() { diff --git a/lix/libutil/file-descriptor.hh b/lix/libutil/file-descriptor.hh index 5331751cb..6c1c698fc 100644 --- a/lix/libutil/file-descriptor.hh +++ b/lix/libutil/file-descriptor.hh @@ -36,6 +36,15 @@ void writeFull(int fd, std::string_view s, bool allowInterrupts = true); */ std::string drainFD(int fd, bool block = true, const size_t reserveSize=0); + +/* + * Will attempt to guess *A* path associated that might lead to the same file as used by this + * file descriptor. + * + * The returned string should NEVER be used as a valid path. + */ +std::string guessOrInventPathFromFD(int fd); + Generator drainFDSource(int fd, bool block = true); class AutoCloseFD @@ -50,6 +59,15 @@ public: AutoCloseFD& operator =(const AutoCloseFD & fd) = delete; AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false); int get() const; + + /* + * Will attempt to guess *A* path associated that might lead to the same file as used by this + * file descriptor. + * + * The returned string should NEVER be used as a valid path. + */ + std::string guessOrInventPath() const { return guessOrInventPathFromFD(fd); } + explicit operator bool() const; int release(); void close(); diff --git a/meson.build b/meson.build index dab2e063f..f10bab621 100644 --- a/meson.build +++ b/meson.build @@ -256,6 +256,11 @@ configdata += { 'HAVE_SECCOMP': seccomp.found().to_int(), } +# fcntl(F_GETPATH) returns the path of an fd on macOS and BSDs +configdata += { + 'HAVE_F_GETPATH': cxx.has_header_symbol('fcntl.h', 'F_GETPATH').to_int(), +} + libarchive = dependency('libarchive', required : true, include_type : 'system') brotli = [ diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index 1349be71f..d1533664e 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -3,6 +3,8 @@ #include "lix/libutil/strings.hh" #include "lix/libutil/types.hh" #include "lix/libutil/terminal.hh" +#include "lix/libutil/unix-domain-socket.hh" +#include "tests/test-data.hh" #include @@ -207,6 +209,85 @@ namespace nix { ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes")); } + /* ---------------------------------------------------------------------------- + * AutoCloseFD::guessOrInventPath + * --------------------------------------------------------------------------*/ + void testGuessOrInventPathPrePostDeletion(AutoCloseFD & fd, Path & path) { + { + SCOPED_TRACE(fmt("guessing path before deletion of '%1%'", path)); + ASSERT_TRUE(fd); + /* We cannot predict what the platform will return here. + * But it cannot fail. */ + ASSERT_TRUE(fd.guessOrInventPath().size() >= 0); + } + { + SCOPED_TRACE(fmt("guessing path after deletion of '%1%'", path)); + deletePath(path); + /* We cannot predict what the platform will return here. + * But it cannot fail. */ + ASSERT_TRUE(fd.guessOrInventPath().size() >= 0); + } + } + TEST(guessOrInventPath, files) { + Path filePath = getUnitTestDataPath("guess-or-invent/test.txt"); + createDirs(dirOf(filePath)); + writeFile(filePath, "some text"); + AutoCloseFD file{open(filePath.c_str(), O_RDONLY, 0666)}; + testGuessOrInventPathPrePostDeletion(file, filePath); + } + + TEST(guessOrInventPath, directories) { + Path dirPath = getUnitTestDataPath("guess-or-invent/test-dir"); + createDirs(dirPath); + AutoCloseFD directory{open(dirPath.c_str(), O_DIRECTORY, 0666)}; + testGuessOrInventPathPrePostDeletion(directory, dirPath); + } + +#ifdef O_PATH + TEST(guessOrInventPath, symlinks) { + Path symlinkPath = getUnitTestDataPath("guess-or-invent/test-symlink"); + Path targetPath = getUnitTestDataPath("guess-or-invent/nowhere"); + createDirs(dirOf(symlinkPath)); + createSymlink(targetPath, symlinkPath); + AutoCloseFD symlink{open(symlinkPath.c_str(), O_PATH | O_NOFOLLOW, 0666)}; + testGuessOrInventPathPrePostDeletion(symlink, symlinkPath); + } + + TEST(guessOrInventPath, fifos) { + Path fifoPath = getUnitTestDataPath("guess-or-invent/fifo"); + createDirs(dirOf(fifoPath)); + ASSERT_TRUE(mkfifo(fifoPath.c_str(), 0666) == 0); + AutoCloseFD fifo{open(fifoPath.c_str(), O_PATH | O_NOFOLLOW, 0666)}; + testGuessOrInventPathPrePostDeletion(fifo, fifoPath); + } +#endif + + TEST(guessOrInventPath, pipes) { + int pipefd[2]; + + ASSERT_TRUE(pipe(pipefd) == 0); + + AutoCloseFD pipe_read{pipefd[0]}; + ASSERT_TRUE(pipe_read); + AutoCloseFD pipe_write{pipefd[1]}; + ASSERT_TRUE(pipe_write); + + /* We cannot predict what the platform will return here. + * But it cannot fail. */ + ASSERT_TRUE(pipe_read.guessOrInventPath().size() >= 0); + ASSERT_TRUE(pipe_write.guessOrInventPath().size() >= 0); + pipe_write.close(); + ASSERT_TRUE(pipe_read.guessOrInventPath().size() >= 0); + pipe_read.close(); + } + + TEST(guessOrInventPath, sockets) { + Path socketPath = getUnitTestDataPath("guess-or-invent/socket"); + createDirs(dirOf(socketPath)); + AutoCloseFD socket = createUnixDomainSocket(socketPath, 0666); + testGuessOrInventPathPrePostDeletion(socket, socketPath); + } + /* ---------------------------------------------------------------------------- * concatStringsSep * --------------------------------------------------------------------------*/