diff --git a/src/libutil-tests/file-system.cc b/src/libutil-tests/file-system.cc index 1551227cb..49c123e79 100644 --- a/src/libutil-tests/file-system.cc +++ b/src/libutil-tests/file-system.cc @@ -388,14 +388,13 @@ TEST(openFileEnsureBeneathNoSymlinks, works) TEST(createAnonymousTempFile, works) { auto fd = createAnonymousTempFile(); - auto fd_ = fromDescriptorReadOnly(fd.get()); writeFull(fd.get(), "test"); - lseek(fd_, 0, SEEK_SET); + lseek(fd.get(), 0, SEEK_SET); FdSource source{fd.get()}; EXPECT_EQ(source.drain(), "test"); - lseek(fd_, 0, SEEK_END); + lseek(fd.get(), 0, SEEK_END); writeFull(fd.get(), "test"); - lseek(fd_, 0, SEEK_SET); + lseek(fd.get(), 0, SEEK_SET); EXPECT_EQ(source.drain(), "testtest"); } @@ -406,9 +405,8 @@ TEST(createAnonymousTempFile, works) TEST(FdSource, restartWorks) { auto fd = createAnonymousTempFile(); - auto fd_ = fromDescriptorReadOnly(fd.get()); writeFull(fd.get(), "hello world"); - lseek(fd_, 0, SEEK_SET); + lseek(fd.get(), 0, SEEK_SET); FdSource source{fd.get()}; EXPECT_EQ(source.drain(), "hello world"); source.restart(); @@ -416,4 +414,11 @@ TEST(FdSource, restartWorks) EXPECT_EQ(source.drain(), ""); } +TEST(createTempDir, works) +{ + auto tmpDir = createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true); + ASSERT_TRUE(std::filesystem::is_directory(tmpDir)); +} + } // namespace nix diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 4851d8cfb..e06b022fc 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -115,9 +115,6 @@ Path canonPath(PathView path, bool resolveSymlinks) if (!isAbsolute(path)) throw Error("not an absolute path: '%1%'", path); - // For Windows - auto rootName = std::filesystem::path{path}.root_name(); - /* This just exists because we cannot set the target of `remaining` (the callback parameter) directly to a newly-constructed string, since it is `std::string_view`. */ @@ -147,8 +144,6 @@ Path canonPath(PathView path, bool resolveSymlinks) } }); - if (!rootName.empty()) - ret = rootName.string() + std::move(ret); return ret; } @@ -380,14 +375,6 @@ void syncParent(const Path & path) fd.fsync(); } -#ifdef __FreeBSD__ -# define MOUNTEDPATHS_PARAM , std::set & mountedPaths -# define MOUNTEDPATHS_ARG , mountedPaths -#else -# define MOUNTEDPATHS_PARAM -# define MOUNTEDPATHS_ARG -#endif - void recursiveSync(const Path & path) { /* If it's a file or symlink, just fsync and return. */ @@ -432,129 +419,6 @@ void recursiveSync(const Path & path) } } -static void _deletePath( - Descriptor parentfd, - const std::filesystem::path & path, - uint64_t & bytesFreed, - std::exception_ptr & ex MOUNTEDPATHS_PARAM) -{ -#ifndef _WIN32 - checkInterrupt(); - -# ifdef __FreeBSD__ - // In case of emergency (unmount fails for some reason) not recurse into mountpoints. - // This prevents us from tearing up the nullfs-mounted nix store. - if (mountedPaths.find(path) != mountedPaths.end()) { - return; - } -# endif - - std::string name(path.filename()); - assert(name != "." && name != ".." && !name.empty()); - - struct stat st; - if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { - if (errno == ENOENT) - return; - throw SysError("getting status of %1%", path); - } - - if (!S_ISDIR(st.st_mode)) { - /* We are about to delete a file. Will it likely free space? */ - - switch (st.st_nlink) { - /* Yes: last link. */ - case 1: - bytesFreed += st.st_size; - break; - /* Maybe: yes, if 'auto-optimise-store' or manual optimisation - was performed. Instead of checking for real let's assume - it's an optimised file and space will be freed. - - In worst case we will double count on freed space for files - with exactly two hardlinks for unoptimised packages. - */ - case 2: - bytesFreed += st.st_size; - break; - /* No: 3+ links. */ - default: - break; - } - } - - if (S_ISDIR(st.st_mode)) { - /* Make the directory accessible. */ - const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; - if ((st.st_mode & PERM_MASK) != PERM_MASK) { - if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) - throw SysError("chmod %1%", path); - } - - int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); - if (fd == -1) - throw SysError("opening directory %1%", path); - AutoCloseDir dir(fdopendir(fd)); - if (!dir) - throw SysError("opening directory %1%", path); - - struct dirent * dirent; - while (errno = 0, dirent = readdir(dir.get())) { /* sic */ - checkInterrupt(); - std::string childName = dirent->d_name; - if (childName == "." || childName == "..") - continue; - _deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG); - } - if (errno) - throw SysError("reading directory %1%", path); - } - - int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; - if (unlinkat(parentfd, name.c_str(), flags) == -1) { - if (errno == ENOENT) - return; - try { - throw SysError("cannot unlink %1%", path); - } catch (...) { - if (!ex) - ex = std::current_exception(); - else - ignoreExceptionExceptInterrupt(); - } - } -#else - // TODO implement - throw UnimplementedError("_deletePath"); -#endif -} - -static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM) -{ - assert(path.is_absolute()); - assert(path.parent_path() != path); - - AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY)); - if (!dirfd) { - if (errno == ENOENT) - return; - throw SysError("opening directory %s", path.parent_path()); - } - - std::exception_ptr ex; - - _deletePath(dirfd.get(), path, bytesFreed, ex MOUNTEDPATHS_ARG); - - if (ex) - std::rethrow_exception(ex); -} - -void deletePath(const std::filesystem::path & path) -{ - uint64_t dummy; - deletePath(path, dummy); -} - void createDir(const Path & path, mode_t mode) { if (mkdir( @@ -577,25 +441,6 @@ void createDirs(const std::filesystem::path & path) } } -void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) -{ - // Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path); -#ifdef __FreeBSD__ - std::set mountedPaths; - struct statfs * mntbuf; - int count; - if ((count = getmntinfo(&mntbuf, MNT_WAIT)) < 0) { - throw SysError("getmntinfo"); - } - - for (int i = 0; i < count; i++) { - mountedPaths.emplace(mntbuf[i].f_mntonname); - } -#endif - bytesFreed = 0; - _deletePath(path, bytesFreed MOUNTEDPATHS_ARG); -} - ////////////////////////////////////////////////////////////////////// AutoDelete::AutoDelete() @@ -672,11 +517,6 @@ void AutoUnmount::cancel() ////////////////////////////////////////////////////////////////////// -std::filesystem::path defaultTempDir() -{ - return getEnvNonEmpty("TMPDIR").value_or("/tmp"); -} - std::filesystem::path createTempDir(const std::filesystem::path & tmpRoot, const std::string & prefix, mode_t mode) { while (1) { diff --git a/src/libutil/include/nix/util/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh index 441ec4d4f..aa6d4e8f0 100644 --- a/src/libutil/include/nix/util/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -261,4 +261,14 @@ Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & p MakeError(EndOfFile, Error); +#ifdef _WIN32 + +/** + * Windows specific replacement for POSIX `lseek` that operates on a `HANDLE` and not + * a file descriptor. + */ +off_t lseek(Descriptor fd, off_t offset, int whence); + +#endif + } // namespace nix diff --git a/src/libutil/include/nix/util/file-path-impl.hh b/src/libutil/include/nix/util/file-path-impl.hh index 91c1a58cd..174eeb1be 100644 --- a/src/libutil/include/nix/util/file-path-impl.hh +++ b/src/libutil/include/nix/util/file-path-impl.hh @@ -40,6 +40,11 @@ struct UnixPathTrait { return path.rfind('/', from); } + + static size_t rootNameLen(StringView) + { + return 0; + } }; /** @@ -83,6 +88,18 @@ struct WindowsPathTrait size_t p2 = path.rfind(preferredSep, from); return p1 == String::npos ? p2 : p2 == String::npos ? p1 : std::max(p1, p2); } + + static size_t rootNameLen(StringView path) + { + if (path.size() >= 2 && path[1] == ':') { + char driveLetter = path[0]; + if ((driveLetter >= 'A' && driveLetter <= 'Z') || (driveLetter >= 'a' && driveLetter <= 'z')) + return 2; + } + /* TODO: This needs to also handle UNC paths. + * https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths */ + return 0; + } }; template @@ -116,6 +133,11 @@ typename PathDict::String canonPathInner(typename PathDict::StringView remaining typename PathDict::String result; result.reserve(256); + if (auto rootNameLength = PathDict::rootNameLen(remaining)) { + result += remaining.substr(0, rootNameLength); /* Copy drive letter verbatim. */ + remaining.remove_prefix(rootNameLength); + } + while (true) { /* Skip slashes. */ diff --git a/src/libutil/include/nix/util/file-system.hh b/src/libutil/include/nix/util/file-system.hh index ba409fac8..ab1a90647 100644 --- a/src/libutil/include/nix/util/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -362,6 +362,8 @@ std::pair createTempFile(const Path & prefix = "nix"); /** * Return `TMPDIR`, or the default temporary directory if unset or empty. + * Uses GetTempPathW on windows which respects TMP, TEMP, USERPROFILE env variables. + * Does not resolve symlinks and the returned path might not be directory or exist at all. */ std::filesystem::path defaultTempDir(); diff --git a/src/libutil/nar-accessor.cc b/src/libutil/nar-accessor.cc index 5644ca408..e43d43aac 100644 --- a/src/libutil/nar-accessor.cc +++ b/src/libutil/nar-accessor.cc @@ -1,4 +1,5 @@ #include "nix/util/nar-accessor.hh" +#include "nix/util/file-descriptor.hh" #include "nix/util/archive.hh" #include @@ -281,7 +282,7 @@ GetNarBytes seekableGetNarBytes(const Path & path) GetNarBytes seekableGetNarBytes(Descriptor fd) { return [fd](uint64_t offset, uint64_t length) { - if (::lseek(fromDescriptorReadOnly(fd), offset, SEEK_SET) == -1) + if (lseek(fd, offset, SEEK_SET) == -1) throw SysError("seeking in file"); std::string buf(length, 0); diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc index 9791b4fed..6af23b2e8 100644 --- a/src/libutil/serialise.cc +++ b/src/libutil/serialise.cc @@ -1,4 +1,5 @@ #include "nix/util/serialise.hh" +#include "nix/util/file-descriptor.hh" #include "nix/util/compression.hh" #include "nix/util/signals.hh" #include "nix/util/util.hh" @@ -162,11 +163,11 @@ size_t FdSource::readUnbuffered(char * data, size_t len) _good = false; throw SysError("reading from file"); } +#endif if (n == 0) { _good = false; throw EndOfFile(std::string(*endOfFileError)); } -#endif read += n; return n; } @@ -207,8 +208,7 @@ void FdSource::restart() throw Error("can't seek to the start of a file"); buffer.reset(); read = bufPosIn = bufPosOut = 0; - int fd_ = fromDescriptorReadOnly(fd); - if (lseek(fd_, 0, SEEK_SET) == -1) + if (lseek(fd, 0, SEEK_SET) == -1) throw SysError("seeking to the start of a file"); } diff --git a/src/libutil/unix/file-system.cc b/src/libutil/unix/file-system.cc index 77b83858f..7086cb258 100644 --- a/src/libutil/unix/file-system.cc +++ b/src/libutil/unix/file-system.cc @@ -9,6 +9,9 @@ #include #include "nix/util/file-system.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" #include "util-unix-config-private.hh" @@ -19,6 +22,11 @@ Descriptor openDirectory(const std::filesystem::path & path) return open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); } +std::filesystem::path defaultTempDir() +{ + return getEnvNonEmpty("TMPDIR").value_or("/tmp"); +} + void setWriteTime( const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) { @@ -66,4 +74,154 @@ void setWriteTime( #endif } +#ifdef __FreeBSD__ +# define MOUNTEDPATHS_PARAM , std::set & mountedPaths +# define MOUNTEDPATHS_ARG , mountedPaths +#else +# define MOUNTEDPATHS_PARAM +# define MOUNTEDPATHS_ARG +#endif + +static void _deletePath( + Descriptor parentfd, + const std::filesystem::path & path, + uint64_t & bytesFreed, + std::exception_ptr & ex MOUNTEDPATHS_PARAM) +{ +#ifndef _WIN32 + checkInterrupt(); + +# ifdef __FreeBSD__ + // In case of emergency (unmount fails for some reason) not recurse into mountpoints. + // This prevents us from tearing up the nullfs-mounted nix store. + if (mountedPaths.find(path) != mountedPaths.end()) { + return; + } +# endif + + std::string name(path.filename()); + assert(name != "." && name != ".." && !name.empty()); + + struct stat st; + if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { + if (errno == ENOENT) + return; + throw SysError("getting status of %1%", path); + } + + if (!S_ISDIR(st.st_mode)) { + /* We are about to delete a file. Will it likely free space? */ + + switch (st.st_nlink) { + /* Yes: last link. */ + case 1: + bytesFreed += st.st_size; + break; + /* Maybe: yes, if 'auto-optimise-store' or manual optimisation + was performed. Instead of checking for real let's assume + it's an optimised file and space will be freed. + + In worst case we will double count on freed space for files + with exactly two hardlinks for unoptimised packages. + */ + case 2: + bytesFreed += st.st_size; + break; + /* No: 3+ links. */ + default: + break; + } + } + + if (S_ISDIR(st.st_mode)) { + /* Make the directory accessible. */ + const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; + if ((st.st_mode & PERM_MASK) != PERM_MASK) { + if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) + throw SysError("chmod %1%", path); + } + + int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (fd == -1) + throw SysError("opening directory %1%", path); + AutoCloseDir dir(fdopendir(fd)); + if (!dir) + throw SysError("opening directory %1%", path); + + struct dirent * dirent; + while (errno = 0, dirent = readdir(dir.get())) { /* sic */ + checkInterrupt(); + std::string childName = dirent->d_name; + if (childName == "." || childName == "..") + continue; + _deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG); + } + if (errno) + throw SysError("reading directory %1%", path); + } + + int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; + if (unlinkat(parentfd, name.c_str(), flags) == -1) { + if (errno == ENOENT) + return; + try { + throw SysError("cannot unlink %1%", path); + } catch (...) { + if (!ex) + ex = std::current_exception(); + else + ignoreExceptionExceptInterrupt(); + } + } +#else + // TODO implement + throw UnimplementedError("_deletePath"); +#endif +} + +static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM) +{ + assert(path.is_absolute()); + assert(path.parent_path() != path); + + AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY)); + if (!dirfd) { + if (errno == ENOENT) + return; + throw SysError("opening directory %s", path.parent_path()); + } + + std::exception_ptr ex; + + _deletePath(dirfd.get(), path, bytesFreed, ex MOUNTEDPATHS_ARG); + + if (ex) + std::rethrow_exception(ex); +} + +void deletePath(const std::filesystem::path & path) +{ + uint64_t dummy; + deletePath(path, dummy); +} + +void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) +{ + // Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path); +#ifdef __FreeBSD__ + std::set mountedPaths; + struct statfs * mntbuf; + int count; + if ((count = getmntinfo(&mntbuf, MNT_WAIT)) < 0) { + throw SysError("getmntinfo"); + } + + for (int i = 0; i < count; i++) { + mountedPaths.emplace(mntbuf[i].f_mntonname); + } +#endif + bytesFreed = 0; + _deletePath(path, bytesFreed MOUNTEDPATHS_ARG); +} + } // namespace nix diff --git a/src/libutil/windows/file-descriptor.cc b/src/libutil/windows/file-descriptor.cc index 3c3e7ea45..57994eac0 100644 --- a/src/libutil/windows/file-descriptor.cc +++ b/src/libutil/windows/file-descriptor.cc @@ -5,13 +5,12 @@ #include "nix/util/windows-error.hh" #include "nix/util/file-path.hh" -#ifdef _WIN32 -# include -# include -# include -# include -# define WIN32_LEAN_AND_MEAN -# include +#include +#include +#include +#include +#define WIN32_LEAN_AND_MEAN +#include namespace nix { @@ -46,16 +45,16 @@ void writeFull(HANDLE handle, std::string_view s, bool allowInterrupts) if (allowInterrupts) checkInterrupt(); DWORD res; -# if _WIN32_WINNT >= 0x0600 +#if _WIN32_WINNT >= 0x0600 auto path = handleToPath(handle); // debug; do it before because handleToPath changes lasterror if (!WriteFile(handle, s.data(), s.size(), &res, NULL)) { throw WinError("writing to file %1%:%2%", handle, path); } -# else +#else if (!WriteFile(handle, s.data(), s.size(), &res, NULL)) { throw WinError("writing to file %1%", handle); } -# endif +#endif if (res > 0) s.remove_prefix(res); } @@ -120,7 +119,7 @@ void Pipe::create() ////////////////////////////////////////////////////////////////////// -# if _WIN32_WINNT >= 0x0600 +#if _WIN32_WINNT >= 0x0600 std::wstring windows::handleToFileName(HANDLE handle) { @@ -149,7 +148,37 @@ Path windows::handleToPath(HANDLE handle) return os_string_to_string(handleToFileName(handle)); } -# endif +#endif + +off_t lseek(HANDLE h, off_t offset, int whence) +{ + DWORD method; + switch (whence) { + case SEEK_SET: + method = FILE_BEGIN; + break; + case SEEK_CUR: + method = FILE_CURRENT; + break; + case SEEK_END: + method = FILE_END; + break; + default: + throw Error("lseek: invalid whence %d", whence); + } + + LARGE_INTEGER li; + li.QuadPart = offset; + LARGE_INTEGER newPos; + + if (!SetFilePointerEx(h, li, &newPos, method)) { + /* Convert to a POSIX error, since caller code works with this as if it were + a POSIX lseek. */ + errno = std::error_code(GetLastError(), std::system_category()).default_error_condition().value(); + return -1; + } + + return newPos.QuadPart; +} } // namespace nix -#endif diff --git a/src/libutil/windows/file-system.cc b/src/libutil/windows/file-system.cc index 22572772e..79931d591 100644 --- a/src/libutil/windows/file-system.cc +++ b/src/libutil/windows/file-system.cc @@ -1,9 +1,11 @@ #include "nix/util/file-system.hh" +#include "nix/util/windows-error.hh" #include "nix/util/logging.hh" -#ifdef _WIN32 namespace nix { +using namespace nix::windows; + void setWriteTime( const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) { @@ -28,5 +30,27 @@ Descriptor openDirectory(const std::filesystem::path & path) NULL); } +std::filesystem::path defaultTempDir() +{ + wchar_t buf[MAX_PATH + 1]; + DWORD len = GetTempPathW(MAX_PATH + 1, buf); + if (len == 0 || len > MAX_PATH) + throw WinError("getting default temporary directory"); + return std::filesystem::path(buf); +} + +void deletePath(const std::filesystem::path & path) +{ + std::error_code ec; + std::filesystem::remove_all(path, ec); + if (ec && ec != std::errc::no_such_file_or_directory) + throw SysError(ec.default_error_condition().value(), "recursively deleting %1%", path); +} + +void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) +{ + bytesFreed = 0; + deletePath(path); +} + } // namespace nix -#endif