Compare commits

...

3 Commits

Author SHA1 Message Date
Sergei Zimmerman
bd1001a752 libutil: Add DirectorySourceAccessor that traverses symlinks without races
Uses openat2 when available on Linux.
2026-02-18 12:48:05 -05:00
Sergei Zimmerman
30008ac474 libutil: Factor out PosixSourceAccessor::makeStat
Useful outside of the PosixSourceAccessor.
2026-02-18 12:32:37 -05:00
John Ericson
a649b0dab7 More file system function improvements
- FD-based creating symlinks on Unix and Windows with wrapper
  (`createFileSymlinkAt`, `createDirectorySymlinkAt`,
  `createUnknownSymlinkAt`).

  Windows distinguishes between file and directory symlinks. Directory
  symlinks can be traversed as path components, file symlinks cannot.
  `createUnknownSymlinkAt` stats the target to determine the type,
  falling back to file symlink if the target doesn't exist.

  `createUnknownSymlinkAt` is a poor solution, and a replacement for
  NARs should be careful to store enough information to avoid needing it
  at unpack time, but it is the best we can do for now.

- Big cleanup of `fs-sink.cc` to take advantage of new stuff and reduce
  CPP.

- Reimplement `lstat` and `maybeLstat` from first principles on Windows,
  so they work with symlinks. Properly define `S_IFLNK` and `S_ISLNK`.

  Use `GetFileAttributesExW` instead of `FindFirstFileW` since we don't
  need wildcard matching.

- Start fixing bugs in Windows now that we can run the tests locally
  with wine decently well enough.

- Refactor `fsync` to be a standalone function, `syncDescriptor`, that
  `AutoCloseFD::fsync()` calls.

  Split into Unix and Windows implementations since `FlushFileBuffers`
  returns `BOOL` (true on success) rather than `int` (-1 on failure).

- Change `writeFile(AutoCloseFD &, ...)` to take a `Descriptor` with
  optional `origPath` parameter (uses `descriptorToPath` if not
  provided).

- Cleanup `*stat` and friends wrappers

  - Implement wrappers for the descriptor-based ones too.

  - Do the `maybe*` ones in terms of the others via try-catch, portably.

Co-authored-by: Sergei Zimmerman <sergei@zimmerman.foo>
2026-02-18 12:29:22 -05:00
25 changed files with 927 additions and 158 deletions

View File

@@ -1,5 +1,6 @@
#include "nix/util/archive.hh"
#include "nix/util/posix-source-accessor.hh"
#include "nix/util/posix-source-accessor.hh"
#include "nix/store/store-api.hh"
#include "nix/store/local-fs-store.hh"
#include "nix/store/globals.hh"
@@ -109,7 +110,7 @@ std::shared_ptr<SourceAccessor> LocalFSStore::getFSAccessor(const StorePath & pa
if (!pathExists(absPath))
return nullptr;
}
return std::make_shared<PosixSourceAccessor>(std::move(absPath));
return makeFSSourceAccessor(std::move(absPath));
}
const std::string LocalFSStore::drvsLogDir = "drvs";

View File

@@ -5,6 +5,7 @@
#include "nix/util/util.hh"
#include "nix/store/store-api.hh"
#include "nix/store/globals.hh"
#include "nix/util/file-system-at.hh"
#include "store-config-private.hh"
#if NIX_SUPPORT_ACL

View File

@@ -758,7 +758,7 @@ std::optional<Descriptor> DerivationBuilderImpl::startBuild()
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
POSIX semantics.*/
tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
tmpDirFd = openDirectory(tmpDir, /*followFinalSymlink=*/false);
if (!tmpDirFd)
throw SysError("failed to open the build temporary directory descriptor %1%", PathFmt(tmpDir));
@@ -1269,7 +1269,7 @@ void DerivationBuilderImpl::writeBuilderFile(const std::string & name, std::stri
openat(tmpDirFd.get(), name.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)};
if (!fd)
throw SysError("creating file %s", PathFmt(path));
writeFile(fd, path, contents);
writeFile(fd.get(), contents);
chownToBuilder(fd.get(), path);
}

View File

@@ -1,5 +1,5 @@
#include "nix/store/pathlocks.hh"
#include "nix/util/file-system.hh"
#include "nix/util/file-system-at.hh"
#include "nix/util/util.hh"
#include "nix/util/sync.hh"
#include "nix/util/signals.hh"

View File

@@ -1,10 +1,11 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "nix/util/file-system-at.hh"
#include "nix/util/file-system.hh"
#include "nix/util/fs-sink.hh"
#include <cstring>
namespace nix {
/* ----------------------------------------------------------------------------

View File

@@ -91,9 +91,7 @@ TEST_F(FSSourceAccessorTest, works)
{
RestoreSink sink(false);
sink.dstPath = tmpDir;
#ifndef _WIN32
sink.dirFd = openDirectory(tmpDir);
#endif
sink.createDirectory(CanonPath("subdir"));
sink.createRegularFile(CanonPath("file1"), [](CreateRegularFileSink & crf) { crf("content1"); });
sink.createRegularFile(CanonPath("subdir/file2"), [](CreateRegularFileSink & crf) { crf("content2"); });
@@ -137,4 +135,34 @@ TEST_F(FSSourceAccessorTest, works)
}
}
/* ----------------------------------------------------------------------------
* RestoreSink non-directory at root (no dirFd)
* --------------------------------------------------------------------------*/
TEST_F(FSSourceAccessorTest, RestoreSinkRegularFileAtRoot)
{
auto filePath = tmpDir / "rootfile";
{
RestoreSink sink(false);
sink.dstPath = filePath;
// No dirFd set - this tests the !dirFd path
sink.createRegularFile(CanonPath::root, [](CreateRegularFileSink & crf) { crf("root content"); });
}
EXPECT_THAT(makeFSSourceAccessor(filePath), HasContents(CanonPath::root, "root content"));
}
TEST_F(FSSourceAccessorTest, RestoreSinkSymlinkAtRoot)
{
auto linkPath = tmpDir / "rootlink";
{
RestoreSink sink(false);
sink.dstPath = linkPath;
// No dirFd set - this tests the !dirFd path
sink.createSymlink(CanonPath::root, "symlink_target");
}
EXPECT_THAT(makeFSSourceAccessor(linkPath), HasSymlink(CanonPath::root, "symlink_target"));
}
} // namespace nix

View File

@@ -0,0 +1,285 @@
// FIXME: detect in meson
#define HAVE_OPENAT2 1
#define HAVE_OPENAT 1
#include "util-config-private.hh"
#include "nix/util/signals.hh"
#include "nix/util/directory-source-accessor.hh"
#include "nix/util/posix-source-accessor.hh"
#if defined(__linux__) && HAVE_OPENAT2
# include <sys/syscall.h>
# include <linux/openat2.h>
#endif
#include <atomic>
#include <ranges>
namespace nix {
#if HAVE_OPENAT
namespace {
class DirFdSourceAccessor : public SourceAccessor
{
/**
* File descriptor of the root directory.
*/
AutoCloseFD dirFd;
/**
* Path corresponding to the accessor.
* @warning Do not use for any file operations!
*/
std::filesystem::path root;
/**
* The most recent mtime seen by lstat(). This is a hack to
* support dumpPathAndGetMtime(). Should remove this eventually.
*/
time_t mtime = 0;
bool trackLastModified = false;
AutoCloseFD openFile(const CanonPath & path, int flags)
{
if (path.isRoot())
return ::openat(dirFd.get(), ".", flags);
# if defined(__linux__) && HAVE_OPENAT2
/* Cache the result of whether openat2 is not supported. */
static std::atomic_flag openat2Unsupported = ATOMIC_FLAG_INIT;
if (!openat2Unsupported.test()) {
/* No glibc wrapper yet, but there's a patch:
* https://patchwork.sourceware.org/project/glibc/patch/20251029200519.3203914-1-adhemerval.zanella@linaro.org/
*/
auto how = ::open_how{
.flags = static_cast<decltype(::open_how::flags)>(flags),
/* Symlinks are disallowed. RESOLVE_BENEATH is a bit overkill, since
CanonPath has the invariant of not having any `..` components, but
that's good practice anyway. */
.resolve = RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH,
};
auto res = ::syscall(__NR_openat2, dirFd.get(), path.rel_c_str(), &how, sizeof(how));
if (res < 0 && errno == ENOSYS) {
/* Cache that the syscall is not supported and fall through to openat. */
openat2Unsupported.test_and_set();
} else {
return res;
}
}
# endif
AutoCloseFD parentFd;
auto nrComponents = std::ranges::distance(path);
auto components = std::views::take(path, nrComponents - 1); /* Everything but last component */
auto getParentFd = [&]() { return parentFd ? parentFd.get() : dirFd.get(); };
/* This rather convoluted loop is necessary to avoid TOCTOU when validating that
no inner path component is a symlink. */
for (auto it = components.begin(); it != components.end(); ++it) {
std::string_view component = *it;
parentFd = ::openat(
getParentFd(), /* First iteration uses dirFd. */
std::string(component).c_str(), /* Copy into a string to make NUL terminated. */
O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC |
# ifdef __linux__
O_PATH /* Linux-specific optimization. Files are open only for path resolution purposes. */
# endif
);
if (!parentFd) {
/* Construct the CanonPath for error message. */
auto path2 = std::ranges::fold_left(components.begin(), ++it, CanonPath::root, [](auto lhs, auto rhs) {
lhs.push(rhs);
return lhs;
});
if (errno == ELOOP)
/* Do not allow any simlinks in internal path components.
This is what is necessary to emulate it without RESOLVE_NO_SYMLINKS. */
throw Error("path '%s' is a symlink", showPath(path2));
return AutoCloseFD();
}
}
return ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags);
}
std::optional<struct ::stat> maybeLstatImpl(const CanonPath & path)
{
std::optional<struct ::stat> st{std::in_place};
if (path.isRoot()) {
if (::fstat(dirFd.get(), &*st)) /* Is already open. */
throw SysError("getting status of '%s'", showPath(path));
return st;
}
auto parentPath = path.parent().value();
auto fd = openFile(parentPath, O_PATH | O_NOFOLLOW | O_CLOEXEC | O_DIRECTORY);
if (!fd) {
if (errno == ENOENT || errno == ENOTDIR)
return std::nullopt;
throw SysError("opening parent path of '%s'", showPath(path));
}
/* Should have the same semantics as lstat on a path. */
if (::fstatat(fd.get(), std::string(path.baseName().value()).c_str(), &*st, AT_SYMLINK_NOFOLLOW)) {
if (errno == ENOENT || errno == ENOTDIR)
st.reset();
else
throw SysError("getting status of '%s'", showPath(path));
}
/* The contract is that trackLastModified implies that the caller uses the accessor
from a single thread. Thus this is not a CAS loop. */
if (trackLastModified)
mtime = std::max(mtime, st->st_mtime);
return st;
}
public:
DirFdSourceAccessor(AutoCloseFD rootFd_, std::filesystem::path root_, bool trackLastModified)
: dirFd(std::move(rootFd_))
, root(std::move(root_))
, trackLastModified(trackLastModified)
{
if (root != root.root_path()) /* Don't prefix root directory. */
displayPrefix = root.string();
}
void readFile(const CanonPath & path, Sink & sink, std::function<void(uint64_t)> sizeCallback) override
{
auto fd = openFile(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
auto size = getFileSize(fd.get());
sizeCallback(size);
drainFD(fd.get(), sink, {.expectedSize = size});
}
bool pathExists(const CanonPath & path) override
{
return maybeLstatImpl(path).has_value();
}
std::optional<Stat> maybeLstat(const CanonPath & path) override
{
// TODO: Deduplicate with PosixSourceAccessor.
auto st = maybeLstatImpl(path);
if (!st)
return std::nullopt;
/* The contract is that trackLastModified implies that the caller uses the accessor
from a single thread. Thus this is not a CAS loop. */
if (trackLastModified)
mtime = std::max(mtime, st->st_mtime);
return PosixSourceAccessor::makeStat(*st);
}
DirEntries readDirectory(const CanonPath & path) override
{
auto fd = openFile(path, O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (!fd)
throw SysError("opening directory '%1%'", path);
auto dir = AutoCloseDir{::fdopendir(fd.get())};
if (!dir)
throw SysError("opening directory '%1%'", path);
fd.release();
DirEntries entries;
struct dirent * dirent;
while (errno = 0, dirent = ::readdir(dir.get())) {
checkInterrupt();
std::string name = dirent->d_name;
if (name == "." || name == "..")
continue;
auto type = [&]() -> std::optional<Type> {
switch (dirent->d_type) {
case DT_REG:
return tRegular;
case DT_DIR:
return tDirectory;
case DT_LNK:
return tSymlink;
case DT_BLK:
return tBlock;
case DT_CHR:
return tChar;
case DT_FIFO:
return tFifo;
case DT_SOCK:
return tSocket;
case DT_UNKNOWN:
default:
return std::nullopt;
}
}();
entries.emplace(std::move(name), type);
}
if (errno)
throw SysError("reading directory '%1%'", showPath(path));
return entries;
}
std::string readLink(const CanonPath & path) override
{
if (path.isRoot())
throw Error("file '%s' is not a symbolic link", path);
auto parentPath = path.parent().value();
auto basename = std::string(path.baseName().value());
auto parentFd = openFile(
parentPath,
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC |
# ifdef __linux__
O_PATH
# endif
);
if (!parentFd)
throw SysError("opening path '%1%'", showPath(parentPath));
std::string target;
target.resize(PATH_MAX);
auto len = ::readlinkat(parentFd.get(), basename.c_str(), target.data(), target.size());
if (len < 0)
throw SysError("reading link '%1%'", showPath(path));
target.resize(len);
return target;
}
std::optional<std::filesystem::path> getPhysicalPath(const CanonPath & path) override
{
return root / path.rel();
}
};
} // namespace
#endif
ref<SourceAccessor> makeDirectorySourceAccessor(AutoCloseFD fd, std::filesystem::path root, bool trackLastModified)
{
#if HAVE_OPENAT
return make_ref<DirFdSourceAccessor>(std::move(fd), std::move(root), trackLastModified);
#else
return make_ref<PosixSourceAccessor>(std::move(root), trackLastModified);
#endif
}
} // namespace nix

View File

@@ -184,24 +184,6 @@ void AutoCloseFD::close()
}
}
void AutoCloseFD::fsync() const
{
if (fd != INVALID_DESCRIPTOR) {
int result;
result =
#ifdef _WIN32
::FlushFileBuffers(fd)
#elif defined(__APPLE__)
::fcntl(fd, F_FULLFSYNC)
#else
::fsync(fd)
#endif
;
if (result == -1)
throw NativeSysError("fsync file descriptor %1%", fd);
}
}
void AutoCloseFD::startFsync() const
{
#ifdef __linux__

View File

@@ -0,0 +1,16 @@
#include "nix/util/file-system-at.hh"
namespace nix {
std::optional<PosixStat> maybeFstatat(Descriptor dirFd, const CanonPath & path)
{
try {
return fstatat(dirFd, path);
} catch (SystemError & e) {
if (e.is(std::errc::no_such_file_or_directory) || e.is(std::errc::not_a_directory))
return std::nullopt;
throw;
}
}
} // namespace nix

View File

@@ -195,71 +195,42 @@ bool isDirOrInDir(const std::filesystem::path & path, const std::filesystem::pat
return path == dir || isInDir(path, dir);
}
#ifdef _WIN32
# define STAT _wstat64
# define LSTAT _wstat64
#else
# define STAT stat
# define LSTAT lstat
#endif
PosixStat stat(const std::filesystem::path & path)
{
PosixStat st;
if (STAT(path.c_str(), &st))
throw SysError("getting status of %s", PathFmt(path));
return st;
}
PosixStat lstat(const std::filesystem::path & path)
{
PosixStat st;
if (LSTAT(path.c_str(), &st))
throw SysError("getting status of %s", PathFmt(path));
return st;
}
PosixStat fstat(int fd)
{
PosixStat st;
if (
#ifdef _WIN32
_fstat64
_wstat64
#else
::fstat
::stat
#endif
(fd, &st))
throw SysError("getting status of fd %d", fd);
(path.c_str(), &st))
throw SysError("getting status of %s", PathFmt(path));
return st;
}
std::optional<PosixStat> maybeStat(const std::filesystem::path & path)
{
std::optional<PosixStat> st{std::in_place};
if (STAT(path.c_str(), &*st)) {
if (errno == ENOENT || errno == ENOTDIR)
st.reset();
else
throw SysError("getting status of %s", PathFmt(path));
try {
return stat(path);
} catch (SystemError & e) {
if (e.is(std::errc::no_such_file_or_directory) || e.is(std::errc::not_a_directory))
return std::nullopt;
throw;
}
return st;
}
std::optional<PosixStat> maybeLstat(const std::filesystem::path & path)
{
std::optional<PosixStat> st{std::in_place};
if (LSTAT(path.c_str(), &*st)) {
if (errno == ENOENT || errno == ENOTDIR)
st.reset();
else
throw SysError("getting status of %s", PathFmt(path));
try {
return lstat(path);
} catch (SystemError & e) {
if (e.is(std::errc::no_such_file_or_directory) || e.is(std::errc::not_a_directory))
return std::nullopt;
throw;
}
return st;
}
#undef STAT
#undef LSTAT
bool pathExists(const std::filesystem::path & path)
{
return maybeLstat(path.string()).has_value();
@@ -340,23 +311,23 @@ void writeFile(const Path & path, std::string_view s, mode_t mode, FsSync sync)
if (!fd)
throw SysError("opening file '%1%'", path);
writeFile(fd, path, s, mode, sync);
writeFile(fd.get(), s, sync, &path);
/* Close explicitly to propagate the exceptions. */
fd.close();
}
void writeFile(AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode, FsSync sync)
void writeFile(Descriptor fd, std::string_view s, FsSync sync, const Path * origPath)
{
assert(fd);
assert(fd != INVALID_DESCRIPTOR);
try {
writeFull(fd.get(), s);
writeFull(fd, s);
if (sync == FsSync::Yes)
fd.fsync();
syncDescriptor(fd);
} catch (Error & e) {
e.addTrace({}, "writing file '%1%'", origPath);
e.addTrace({}, "writing file '%1%'", origPath ? *origPath : descriptorToPath(fd).string());
throw;
}
}

View File

@@ -105,34 +105,26 @@ void RestoreSink::createDirectory(const CanonPath & path)
{
auto p = append(dstPath, path);
#ifndef _WIN32
if (dirFd) {
if (path.isRoot())
/* Trying to create a directory that we already have a file descriptor for. */
throw Error("path %s already exists", PathFmt(p));
if (::mkdirat(dirFd.get(), path.rel_c_str(), 0777) == -1)
throw SysError("creating directory %s", PathFmt(p));
createDirectoryAt(dirFd.get(), path);
return;
}
#endif
if (!std::filesystem::create_directory(p))
throw Error("path '%s' already exists", p.string());
#ifndef _WIN32
if (path.isRoot()) {
assert(!dirFd); // Handled above
/* Open directory for further *at operations relative to the sink root
directory. */
dirFd = open(p.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (!dirFd)
throw SysError("creating directory %1%", PathFmt(p));
dirFd = openDirectory(p, /*followFinalSymlink=*/false);
}
#endif
};
}
struct RestoreRegularFile : CreateRegularFileSink, FdSink
{
@@ -164,28 +156,26 @@ void RestoreSink::createRegularFile(const CanonPath & path, std::function<void(C
{
auto p = append(dstPath, path);
auto crf = RestoreRegularFile(
startFsync,
auto crf = RestoreRegularFile(startFsync, [&]() -> Descriptor {
AutoCloseFD parentFd;
if (!dirFd) {
assert(path.isRoot());
assert(p.has_parent_path());
parentFd = openDirectory(p.parent_path());
}
return openFileEnsureBeneathNoSymlinks(
dirFd ? dirFd.get() : parentFd.get(),
dirFd ? path : CanonPath::root / p.filename().string(),
#ifdef _WIN32
CreateFileW(
p.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL)
FILE_NON_DIRECTORY_FILE,
FILE_CREATE
#else
[&]() {
/* O_EXCL together with O_CREAT ensures symbolic links in the last
component are not followed. */
constexpr int flags = O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC;
if (!dirFd)
return ::open(p.c_str(), flags, 0666);
return openFileEnsureBeneathNoSymlinks(dirFd.get(), path, flags, 0666);
}()
O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC,
0666
#endif
);
);
}());
if (!crf.fd)
throw NativeSysError("creating file %1%", PathFmt(p));
func(crf);
@@ -224,14 +214,16 @@ void RestoreRegularFile::preallocateContents(uint64_t len)
void RestoreSink::createSymlink(const CanonPath & path, const std::string & target)
{
auto p = append(dstPath, path);
#ifndef _WIN32
if (dirFd) {
if (::symlinkat(requireCString(target), dirFd.get(), path.rel_c_str()) == -1)
throw SysError("creating symlink from %1% -> '%2%'", PathFmt(p), target);
return;
AutoCloseFD parentFd;
if (!dirFd) {
assert(path.isRoot());
assert(p.has_parent_path());
parentFd = openDirectory(p.parent_path());
}
#endif
nix::createSymlink(target, p.string());
createUnknownSymlinkAt(
dirFd ? dirFd.get() : parentFd.get(),
dirFd ? path : CanonPath::root / p.filename().string(),
string_to_os_string(target));
}
void RegularFileSink::createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func)

View File

@@ -0,0 +1,25 @@
#pragma once
/// @file
#include "nix/util/source-accessor.hh"
#include "nix/util/ref.hh"
#include <filesystem>
namespace nix {
/**
* Create a directory accessor rooted at @param root.
*
* On unix accesses are performed with openat. Linux uses openat2 if supported
* by the kernel (>= 5.6).
*
* On Windows returns a PosixSourceAccessor.
*
* @param fd Descriptor of the directory.
* @param root Filesystem path corresponding to fd. Must correspond to a directory.
*/
ref<SourceAccessor>
makeDirectorySourceAccessor(AutoCloseFD fd, std::filesystem::path root, bool trackLastModified = false);
} // namespace nix

View File

@@ -133,6 +133,11 @@ std::string readLine(Descriptor fd, bool eofOk = false, char terminator = '\n');
*/
void writeLine(Descriptor fd, std::string s);
/**
* Perform a blocking fsync operation on a file descriptor.
*/
void syncDescriptor(Descriptor fd);
/**
* Options for draining a file descriptor to a sink.
*/
@@ -253,7 +258,11 @@ public:
/**
* Perform a blocking fsync operation.
*/
void fsync() const;
void fsync() const
{
if (fd != INVALID_DESCRIPTOR)
nix::syncDescriptor(fd);
}
/**
* Asynchronously flush to disk without blocking, if available on

View File

@@ -15,6 +15,7 @@
*/
#include "nix/util/file-descriptor.hh"
#include "nix/util/file-system.hh"
#include <optional>
@@ -33,6 +34,90 @@ namespace nix {
*/
OsString readLinkAt(Descriptor dirFd, const CanonPath & path);
/**
* Create a symlink to a file relative to a directory file descriptor.
*
* On Windows, creates a file symlink. On Unix, equivalent to symlinkat.
*
* @param dirFd Directory file descriptor
* @param path Relative path for the new symlink
* @param target The symlink target (what it points to)
*
* @throws SystemError on any I/O errors.
*/
void createFileSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target);
/**
* Create a symlink to a directory relative to a directory file descriptor.
*
* On Windows, creates a directory symlink. On Unix, equivalent to symlinkat.
*
* @param dirFd Directory file descriptor
* @param path Relative path for the new symlink
* @param target The symlink target (what it points to)
*
* @throws SystemError on any I/O errors.
*/
void createDirectorySymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target);
/**
* Create a symlink relative to a directory file descriptor, detecting target type.
*
* On Windows, stats the target to determine whether to create a file or
* directory symlink. Falls back to file symlink if the target does not exist.
* On Unix, equivalent to symlinkat.
*
* @param dirFd Directory file descriptor
* @param path Relative path for the new symlink
* @param target The symlink target (what it points to)
*
* @throws SystemError on any I/O errors.
*/
void createUnknownSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target);
/**
* Create a directory relative to a directory file descriptor.
*
* @param dirFd Directory file descriptor
* @param path Relative path for the new directory
*
* @throws SystemError on any I/O errors.
*/
void createDirectoryAt(Descriptor dirFd, const CanonPath & path);
/**
* Get status of an open file/directory handle.
*
* @param fd File descriptor/handle
* @throws SystemError on I/O errors.
*/
PosixStat fstat(Descriptor fd);
/**
* Get status of a file relative to a directory file descriptor.
*
* @param dirFd Directory file descriptor
* @param path Relative path to stat
*
* @return nullopt if the path does not exist.
* @throws SystemError on other I/O errors.
*
* @pre path.isRoot() is false
*/
std::optional<PosixStat> maybeFstatat(Descriptor dirFd, const CanonPath & path);
/**
* Get status of a file relative to a directory file descriptor.
*
* @param dirFd Directory file descriptor
* @param path Relative path to stat
*
* @throws SystemError if the path does not exist or on other I/O errors.
*
* @pre path.isRoot() is false
*/
PosixStat fstatat(Descriptor dirFd, const CanonPath & path);
/**
* Safe(r) function to open a file relative to dirFd, while
* disallowing escaping from a directory and any symlinks in the process.

View File

@@ -28,11 +28,20 @@
* Polyfill for MinGW
*
* Windows does in fact support symlinks, but the C runtime interfaces predate this.
*
* @todo get rid of this, and stop using `stat` when we want `lstat` too.
* We define S_IFLNK and S_ISLNK so that our lstat implementation can properly
* indicate symlinks by setting these mode bits when it detects a reparse point.
*/
#ifndef S_IFLNK
# ifndef _WIN32
# error "S_IFLNK should be defined on non-Windows platforms"
# endif
# define S_IFLNK 0120000
#endif
#ifndef S_ISLNK
# define S_ISLNK(m) false
# ifndef _WIN32
# error "S_ISLNK should be defined on non-Windows platforms"
# endif
# define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#endif
namespace nix {
@@ -125,6 +134,38 @@ using PosixStat =
#endif
;
#ifdef _WIN32
namespace windows {
/**
* Convert Windows FILETIME to Unix time_t.
*/
time_t fileTimeToUnixTime(const FILETIME & ft);
/**
* Fill a PosixStat structure from file attributes and timestamps.
*
* @param dwFileAttributes File attributes (FILE_ATTRIBUTE_*)
* @param ftCreationTime Creation time
* @param ftLastAccessTime Last access time
* @param ftLastWriteTime Last write time
* @param nFileSizeHigh High 32 bits of file size
* @param nFileSizeLow Low 32 bits of file size
* @param nNumberOfLinks Number of hard links (default 1)
*/
void statFromFileInfo(
PosixStat & st,
DWORD dwFileAttributes,
const FILETIME & ftCreationTime,
const FILETIME & ftLastAccessTime,
const FILETIME & ftLastWriteTime,
DWORD nFileSizeHigh,
DWORD nFileSizeLow,
DWORD nNumberOfLinks = 1);
} // namespace windows
#endif
/**
* Get status of `path`.
*/
@@ -133,10 +174,6 @@ PosixStat lstat(const std::filesystem::path & path);
* Get status of `path` following symlinks.
*/
PosixStat stat(const std::filesystem::path & path);
/**
* Get status of an open file descriptor.
*/
PosixStat fstat(int fd);
/**
* `lstat` the given path if it exists.
* @return std::nullopt if the path doesn't exist, or an optional containing the result of `lstat` otherwise
@@ -205,8 +242,10 @@ std::filesystem::path descriptorToPath(Descriptor fd);
/**
* Open a `Descriptor` with read-only access to the given directory.
*
* @param followSymlinks If false, fail if the path is a symlink.
*/
Descriptor openDirectory(const std::filesystem::path & path);
Descriptor openDirectory(const std::filesystem::path & path, bool followFinalSymlink = true);
/**
* Open a `Descriptor` with read-only access to the given file.
@@ -266,8 +305,7 @@ writeFile(const std::filesystem::path & path, Source & source, mode_t mode = 066
return writeFile(path.string(), source, mode, sync);
}
void writeFile(
AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode = 0666, FsSync sync = FsSync::No);
void writeFile(Descriptor fd, std::string_view s, FsSync sync = FsSync::No, const Path * origPath = nullptr);
/**
* Flush a path's parent directory to disk.

View File

@@ -32,6 +32,7 @@ headers = [ config_pub_h ] + files(
'configuration.hh',
'current-process.hh',
'demangle.hh',
'directory-source-accessor.hh',
'english.hh',
'environment-variables.hh',
'error.hh',

View File

@@ -82,6 +82,8 @@ public:
void invalidateCache(const CanonPath & path) override;
static Stat makeStat(const struct ::stat st);
private:
/**

View File

@@ -139,6 +139,7 @@ sources = [ config_priv_h ] + files(
'config-global.cc',
'configuration.cc',
'current-process.cc',
'directory-source-accessor.cc',
'english.cc',
'environment-variables.cc',
'error.cc',
@@ -147,6 +148,7 @@ sources = [ config_priv_h ] + files(
'experimental-features.cc',
'file-content-address.cc',
'file-descriptor.cc',
'file-system-at.cc',
'file-system.cc',
'fs-sink.cc',
'git.cc',

View File

@@ -1,4 +1,5 @@
#include "nix/util/posix-source-accessor.hh"
#include "nix/util/directory-source-accessor.hh"
#include "nix/util/source-path.hh"
#include "nix/util/signals.hh"
#include "nix/util/sync.hh"
@@ -108,22 +109,7 @@ std::optional<SourceAccessor::Stat> PosixSourceAccessor::maybeLstat(const CanonP
if (trackLastModified)
mtime = std::max(mtime, st->st_mtime);
return Stat{
.type = S_ISREG(st->st_mode) ? tRegular
: S_ISDIR(st->st_mode) ? tDirectory
: S_ISLNK(st->st_mode) ? tSymlink
: S_ISCHR(st->st_mode) ? tChar
: S_ISBLK(st->st_mode) ? tBlock
:
#ifdef S_ISSOCK
S_ISSOCK(st->st_mode) ? tSocket
:
#endif
S_ISFIFO(st->st_mode) ? tFifo
: tUnknown,
.fileSize = S_ISREG(st->st_mode) ? std::optional<uint64_t>(st->st_size) : std::nullopt,
.isExecutable = S_ISREG(st->st_mode) && st->st_mode & S_IXUSR,
};
return makeStat(*st);
}
SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath & path)
@@ -200,6 +186,26 @@ void PosixSourceAccessor::assertNoSymlinks(CanonPath path)
}
}
PosixSourceAccessor::Stat PosixSourceAccessor::makeStat(const struct ::stat st)
{
return Stat{
.type = S_ISREG(st.st_mode) ? tRegular
: S_ISDIR(st.st_mode) ? tDirectory
: S_ISLNK(st.st_mode) ? tSymlink
: S_ISCHR(st.st_mode) ? tChar
: S_ISBLK(st.st_mode) ? tBlock
:
#ifdef S_ISSOCK
S_ISSOCK(st.st_mode) ? tSocket
:
#endif
S_ISFIFO(st.st_mode) ? tFifo
: tUnknown,
.fileSize = S_ISREG(st.st_mode) ? std::optional<uint64_t>(st.st_size) : std::nullopt,
.isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR,
};
}
ref<SourceAccessor> getFSSourceAccessor()
{
static auto rootFS = make_ref<PosixSourceAccessor>();
@@ -208,6 +214,30 @@ ref<SourceAccessor> getFSSourceAccessor()
ref<SourceAccessor> makeFSSourceAccessor(std::filesystem::path root, bool trackLastModified)
{
AutoCloseFD fd = toDescriptor(
::open(
root.c_str(),
O_RDONLY |
#ifndef _WIN32
O_NOFOLLOW | O_CLOEXEC
#endif
));
if (!fd) {
if (errno == ELOOP)
/* Use a plain old PosixSourceAccessor for symlinks. Should probably just
read the link here into a MemorySourceAccessor. */
return make_ref<PosixSourceAccessor>(std::move(root), trackLastModified);
throw SysError("opening file %s", PathFmt(root));
}
struct ::stat st;
if (::fstat(fd.get(), &st))
throw SysError("getting status of %s", PathFmt(root));
if (S_ISDIR(st.st_mode))
return makeDirectorySourceAccessor(std::move(fd), std::move(root), trackLastModified);
return make_ref<PosixSourceAccessor>(std::move(root), trackLastModified);
}
} // namespace nix

View File

@@ -1,4 +1,5 @@
#include "nix/util/file-system.hh"
#include "nix/util/file-system-at.hh"
#include "nix/util/signals.hh"
#include "nix/util/finally.hh"
#include "nix/util/serialise.hh"
@@ -207,4 +208,17 @@ void unix::closeOnExec(int fd)
throw SysError("setting close-on-exec flag");
}
void syncDescriptor(Descriptor fd)
{
int result =
#if defined(__APPLE__)
::fcntl(fd, F_FULLFSYNC)
#else
::fsync(fd)
#endif
;
if (result == -1)
throw NativeSysError("fsync file descriptor %1%", fd);
}
} // namespace nix

View File

@@ -23,6 +23,8 @@
# define HAVE_FCHMODAT2 0
#endif
#include "util-unix-config-private.hh"
namespace nix {
#ifdef __linux__
@@ -90,10 +92,9 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
PathFmt(descriptorToPath(dirFd) / path.rel()));
}
struct ::stat st;
/* Possible since https://github.com/torvalds/linux/commit/55815f70147dcfa3ead5738fd56d3574e2e3c1c2 (3.6) */
if (::fstat(pathFd.get(), &st) == -1)
throw SysError("statting '%s' relative to parent directory via O_PATH file descriptor", path.rel());
/* Possible to use with O_PATH fd since
* https://github.com/torvalds/linux/commit/55815f70147dcfa3ead5738fd56d3574e2e3c1c2 (3.6) */
auto st = fstat(pathFd.get());
if (S_ISLNK(st.st_mode))
throw SysError(EOPNOTSUPP, "can't change mode of symlink %s", PathFmt(descriptorToPath(dirFd) / path.rel()));
@@ -171,8 +172,7 @@ openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & pat
});
if (errno == ENOTDIR) /* Path component might be a symlink. */ {
struct ::stat st;
if (::fstatat(getParentFd(), component.c_str(), &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(st.st_mode))
if (auto st = maybeFstatat(getParentFd(), CanonPath(component)); st && S_ISLNK(st->st_mode))
throw SymlinkNotAllowed(path2);
errno = ENOTDIR; /* Restore the errno. */
} else if (errno == ELOOP) {
@@ -224,4 +224,61 @@ OsString readLinkAt(Descriptor dirFd, const CanonPath & path)
}
}
static void symlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
if (::symlinkat(target.c_str(), dirFd, path.rel_c_str()) == -1) {
auto savedErrno = errno;
throw SysError(
savedErrno, "creating symlink %1% -> %2%", PathFmt(descriptorToPath(dirFd) / path.rel()), target);
}
}
void createFileSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
symlinkAt(dirFd, path, target);
}
void createDirectorySymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
symlinkAt(dirFd, path, target);
}
void createUnknownSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
symlinkAt(dirFd, path, target);
}
void createDirectoryAt(Descriptor dirFd, const CanonPath & path)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
if (::mkdirat(dirFd, path.rel_c_str(), 0777) == -1) {
auto savedErrno = errno;
throw SysError(savedErrno, "creating directory %1%", PathFmt(descriptorToPath(dirFd) / path.rel()));
}
}
PosixStat fstat(Descriptor fd)
{
PosixStat st;
if (::fstat(fd, &st)) {
auto savedErrno = errno;
throw SysError(savedErrno, "getting status of %s", PathFmt(descriptorToPath(fd)));
}
return st;
}
PosixStat fstatat(Descriptor dirFd, const CanonPath & path)
{
assert(!path.isRoot());
PosixStat st;
if (::fstatat(dirFd, path.rel_c_str(), &st, AT_SYMLINK_NOFOLLOW)) {
auto savedErrno = errno;
throw SysError(savedErrno, "getting status of %s", PathFmt(descriptorToPath(dirFd) / path.rel()));
}
return st;
}
} // namespace nix

View File

@@ -23,9 +23,9 @@
namespace nix {
Descriptor openDirectory(const std::filesystem::path & path)
Descriptor openDirectory(const std::filesystem::path & path, bool followFinalSymlink)
{
return open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
return open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | (followFinalSymlink ? 0 : O_NOFOLLOW));
}
Descriptor openFileReadonly(const std::filesystem::path & path)
@@ -76,6 +76,14 @@ std::filesystem::path defaultTempDir()
return getEnvNonEmpty("TMPDIR").value_or("/tmp");
}
PosixStat lstat(const std::filesystem::path & path)
{
PosixStat st;
if (::lstat(path.c_str(), &st))
throw SysError("getting status of %s", PathFmt(path));
return st;
}
void setWriteTime(
const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional<bool> optIsSymlink)
{

View File

@@ -148,4 +148,10 @@ off_t lseek(HANDLE h, off_t offset, int whence)
return newPos.QuadPart;
}
void syncDescriptor(Descriptor fd)
{
if (!::FlushFileBuffers(fd))
throw WinError("FlushFileBuffers file descriptor %1%", fd);
}
} // namespace nix

View File

@@ -4,8 +4,9 @@
#include "nix/util/file-path.hh"
#include "nix/util/source-accessor.hh"
#include <span>
#include <fileapi.h>
#include <error.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winioctl.h>
@@ -88,8 +89,8 @@ HANDLE openSymlinkAt(Descriptor dirFd, const CanonPath & path)
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
std::wstring wpath = string_to_os_string(path.rel());
return ntOpenAt(dirFd, wpath, FILE_READ_ATTRIBUTES | SYNCHRONIZE, FILE_OPEN_REPARSE_POINT);
auto wpath = std::filesystem::path(path.rel()).make_preferred();
return ntOpenAt(dirFd, wpath.c_str(), FILE_READ_ATTRIBUTES | SYNCHRONIZE, FILE_OPEN_REPARSE_POINT);
}
/**
@@ -191,6 +192,54 @@ OsString readSymlinkTarget(HANDLE linkHandle)
return {target_start, target_len};
}
/**
* Write symlink target to an open file handle using reparse point.
*
* @param handle Open file handle (must have GENERIC_WRITE access)
* @param target The symlink target (what it points to)
*/
void writeSymlinkTarget(HANDLE handle, const std::filesystem::path & target)
{
/* Build the reparse data buffer for a symbolic link.
Layout: SubstituteName and PrintName stored consecutively in PathBuffer.
We use the same string for both. */
size_t targetBytes = target.native().size() * sizeof(wchar_t);
size_t bufSize = offsetof(ReparseDataBuffer, SymbolicLinkReparseBuffer.PathBuffer) + targetBytes * 2;
std::vector<uint8_t> buf(bufSize, 0);
auto * reparse = reinterpret_cast<ReparseDataBuffer *>(buf.data());
reparse->ReparseTag = IO_REPARSE_TAG_SYMLINK;
reparse->ReparseDataLength =
static_cast<unsigned short>(bufSize - offsetof(ReparseDataBuffer, SymbolicLinkReparseBuffer));
reparse->Reserved = 0;
auto & symlink = reparse->SymbolicLinkReparseBuffer;
/* SubstituteName comes first */
symlink.SubstituteNameOffset = 0;
symlink.SubstituteNameLength = static_cast<unsigned short>(targetBytes);
/* PrintName follows SubstituteName */
symlink.PrintNameOffset = static_cast<unsigned short>(targetBytes);
symlink.PrintNameLength = static_cast<unsigned short>(targetBytes);
/* SYMLINK_FLAG_RELATIVE = 1 for relative symlinks, 0 for absolute */
symlink.Flags = target.is_relative() ? 1 : 0;
/* Copy target into PathBuffer twice (SubstituteName and PrintName) */
memcpy(symlink.PathBuffer, target.c_str(), targetBytes);
memcpy(reinterpret_cast<char *>(symlink.PathBuffer) + targetBytes, target.c_str(), targetBytes);
DWORD bytesReturned;
if (!DeviceIoControl(
handle,
FSCTL_SET_REPARSE_POINT,
buf.data(),
static_cast<DWORD>(bufSize),
nullptr,
0,
&bytesReturned,
nullptr))
throw WinError("setting reparse point for symlink");
}
/**
* Check if a handle refers to a reparse point (e.g., symlink).
*
@@ -210,6 +259,115 @@ bool isReparsePoint(HANDLE handle)
} // namespace windows
OsString readLinkAt(Descriptor dirFd, const CanonPath & path)
{
AutoCloseFD linkHandle(openSymlinkAt(dirFd, path));
return readSymlinkTarget(linkHandle.get());
}
void createFileSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
auto wpath = std::filesystem::path(path.rel()).make_preferred();
/* Create the file that will become the symlink */
AutoCloseFD handle(ntOpenAt(
dirFd, wpath.c_str(), GENERIC_WRITE | DELETE, FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT, FILE_CREATE));
writeSymlinkTarget(handle.get(), target);
}
void createDirectorySymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
auto wpath = std::filesystem::path(path.rel()).make_preferred();
/* Create the directory that will become the symlink */
AutoCloseFD handle(ntOpenAt(
dirFd, wpath.c_str(), GENERIC_WRITE | DELETE, FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT, FILE_CREATE));
writeSymlinkTarget(handle.get(), target);
}
void createUnknownSymlinkAt(Descriptor dirFd, const CanonPath & path, const OsString & target)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
/* Resolve target path to determine if it's a directory.
Target is relative to the symlink's parent directory. */
std::filesystem::path targetPath(target);
std::filesystem::path resolvedTarget;
if (targetPath.is_absolute()) {
resolvedTarget = targetPath;
} else {
/* Get the symlink's parent directory path and resolve target relative to it */
auto symlinkDir = descriptorToPath(dirFd);
if (auto parent = path.parent())
symlinkDir /= parent->rel();
resolvedTarget = symlinkDir / targetPath;
}
/* Check if target is a directory. If we can't stat it (e.g., dangling symlink),
fall back to file symlink. */
std::error_code ec;
auto status = std::filesystem::status(resolvedTarget, ec);
bool isDirectory = !ec && std::filesystem::is_directory(status);
if (isDirectory)
createDirectorySymlinkAt(dirFd, path, target);
else
createFileSymlinkAt(dirFd, path, target);
}
void createDirectoryAt(Descriptor dirFd, const CanonPath & path)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
auto wpath = std::filesystem::path(path.rel()).make_preferred();
/* Create the directory. The handle is immediately closed. */
AutoCloseFD handle(ntOpenAt(dirFd, wpath.c_str(), FILE_TRAVERSE | SYNCHRONIZE, FILE_DIRECTORY_FILE, FILE_CREATE));
}
PosixStat fstat(Descriptor fd)
{
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(fd, &info))
throw WinError("getting file information for %s", PathFmt(descriptorToPath(fd)));
PosixStat st;
windows::statFromFileInfo(
st,
info.dwFileAttributes,
info.ftCreationTime,
info.ftLastAccessTime,
info.ftLastWriteTime,
info.nFileSizeHigh,
info.nFileSizeLow,
info.nNumberOfLinks);
return st;
}
PosixStat fstatat(Descriptor dirFd, const CanonPath & path)
{
assert(!path.isRoot());
auto wpath = std::filesystem::path(path.rel()).make_preferred();
/* Open the file without following symlinks */
AutoCloseFD handle(ntOpenAt(dirFd, wpath.c_str(), FILE_READ_ATTRIBUTES | SYNCHRONIZE, FILE_OPEN_REPARSE_POINT));
return fstat(handle.get());
}
Descriptor openFileEnsureBeneathNoSymlinks(
Descriptor dirFd, const CanonPath & path, ACCESS_MASK desiredAccess, ULONG createOptions, ULONG createDisposition)
{
@@ -301,10 +459,4 @@ Descriptor openFileEnsureBeneathNoSymlinks(
return finalHandle;
}
OsString readLinkAt(Descriptor dirFd, const CanonPath & path)
{
AutoCloseFD linkHandle(windows::openSymlinkAt(dirFd, path));
return windows::readSymlinkTarget(linkHandle.get());
}
} // namespace nix

View File

@@ -23,7 +23,7 @@ void setWriteTime(
warn("Changing file times is not yet implemented on Windows, path is %s", PathFmt(path));
}
Descriptor openDirectory(const std::filesystem::path & path)
Descriptor openDirectory(const std::filesystem::path & path, bool followFinalSymlink)
{
return CreateFileW(
path.c_str(),
@@ -31,7 +31,7 @@ Descriptor openDirectory(const std::filesystem::path & path)
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
/*lpSecurityAttributes=*/nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_BACKUP_SEMANTICS | (followFinalSymlink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT),
/*hTemplateFile=*/nullptr);
}
@@ -105,4 +105,67 @@ std::filesystem::path descriptorToPath(Descriptor handle)
return std::filesystem::path{std::wstring{buf.data(), dw}};
}
time_t windows::fileTimeToUnixTime(const FILETIME & ft)
{
/* FILETIME is 100-nanosecond intervals since January 1, 1601 UTC.
Unix time is seconds since January 1, 1970 UTC.
Difference is 11644473600 seconds. */
ULARGE_INTEGER ull;
ull.LowPart = ft.dwLowDateTime;
ull.HighPart = ft.dwHighDateTime;
return static_cast<time_t>(ull.QuadPart / 10000000ULL - 11644473600ULL);
}
void windows::statFromFileInfo(
PosixStat & st,
DWORD dwFileAttributes,
const FILETIME & ftCreationTime,
const FILETIME & ftLastAccessTime,
const FILETIME & ftLastWriteTime,
DWORD nFileSizeHigh,
DWORD nFileSizeLow,
DWORD nNumberOfLinks)
{
memset(&st, 0, sizeof(st));
/* Determine file type */
if (dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
st.st_mode = S_IFLNK | 0777;
} else if (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
st.st_mode = S_IFDIR | 0755;
} else {
st.st_mode = S_IFREG | 0644;
}
/* File size (only meaningful for regular files) */
st.st_size = (static_cast<int64_t>(nFileSizeHigh) << 32) | nFileSizeLow;
/* Timestamps */
st.st_atime = fileTimeToUnixTime(ftLastAccessTime);
st.st_mtime = fileTimeToUnixTime(ftLastWriteTime);
st.st_ctime = fileTimeToUnixTime(ftCreationTime);
st.st_nlink = nNumberOfLinks;
st.st_uid = 0;
st.st_gid = 0;
}
PosixStat lstat(const std::filesystem::path & path)
{
WIN32_FILE_ATTRIBUTE_DATA attrData;
if (!GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &attrData))
throw WinError("getting status of %s", PathFmt(path));
PosixStat st;
windows::statFromFileInfo(
st,
attrData.dwFileAttributes,
attrData.ftCreationTime,
attrData.ftLastAccessTime,
attrData.ftLastWriteTime,
attrData.nFileSizeHigh,
attrData.nFileSizeLow);
return st;
}
} // namespace nix