From 0848a16ccebc5ad0407ef59353fade7c70ac6577 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 --- meson.build | 5 ++ src/libutil/file-descriptor.cc | 23 ++++++ src/libutil/file-descriptor.hh | 18 +++++ tests/unit/libutil-support/tests/test-data.cc | 16 ++++ tests/unit/libutil-support/tests/test-data.hh | 20 +++++ tests/unit/libutil/tests.cc | 81 +++++++++++++++++++ tests/unit/meson.build | 1 + 7 files changed, 164 insertions(+) create mode 100644 tests/unit/libutil-support/tests/test-data.cc create mode 100644 tests/unit/libutil-support/tests/test-data.hh diff --git a/meson.build b/meson.build index cf1b877f9..703fc8684 100644 --- a/meson.build +++ b/meson.build @@ -227,6 +227,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/src/libutil/file-descriptor.cc b/src/libutil/file-descriptor.cc index 8385ea402..c31ca3fb9 100644 --- a/src/libutil/file-descriptor.cc +++ b/src/libutil/file-descriptor.cc @@ -156,6 +156,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/src/libutil/file-descriptor.hh b/src/libutil/file-descriptor.hh index 7270b73b5..856bd330f 100644 --- a/src/libutil/file-descriptor.hh +++ b/src/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/tests/unit/libutil-support/tests/test-data.cc b/tests/unit/libutil-support/tests/test-data.cc new file mode 100644 index 000000000..016bd5e78 --- /dev/null +++ b/tests/unit/libutil-support/tests/test-data.cc @@ -0,0 +1,16 @@ +#include "test-data.hh" +#include "strings.hh" + +namespace nix { + +Path getUnitTestData() +{ + return getEnv("_NIX_TEST_UNIT_DATA").value(); +} + +Path getUnitTestDataPath(std::string_view path) +{ + return absPath(getUnitTestData() + "/" + path); +} + +} diff --git a/tests/unit/libutil-support/tests/test-data.hh b/tests/unit/libutil-support/tests/test-data.hh new file mode 100644 index 000000000..794ec6265 --- /dev/null +++ b/tests/unit/libutil-support/tests/test-data.hh @@ -0,0 +1,20 @@ +#pragma once + +#include "types.hh" +#include "environment-variables.hh" +#include "file-system.hh" + +namespace nix { + +/** + * The path to the unit test data directory. See the contributing guide + * in the manual for further details. + */ +Path getUnitTestData(); + +/** + * Resolve a path under the unit test data directory to an absolute path. + */ +Path getUnitTestDataPath(std::string_view path); + +} diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index 29a5f7164..c22436257 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -3,6 +3,8 @@ #include "strings.hh" #include "types.hh" #include "terminal.hh" +#include "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 * --------------------------------------------------------------------------*/ diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 55c7566bd..a8a4eee01 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -19,6 +19,7 @@ libutil_test_support_sources = files( 'libutil-support/tests/cli-literate-parser.cc', 'libutil-support/tests/hash.cc', 'libutil-support/tests/terminal-code-eater.cc', + 'libutil-support/tests/test-data.cc', ) libutil_test_support = library( 'lixutil-test-support',