// fs.hpp on Windows, over Win32's wide calls. A directory link is a junction, which needs no // admin rights or Developer Mode where a symlink does; Win32 has no call that makes one, so the // reparse point is written here. A junction is removed as an empty directory is, which removes // the junction and never what it points at. AI-authored (Claude). #ifdef _WIN32 #include "fs.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace packutils::fs { namespace { std::wstring wide(std::string_view utf8) { if (utf8.empty()) { return {}; } const int units = ::MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); std::wstring out(static_cast(units), L'\0'); ::MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), out.data(), units); return out; } std::string narrow(std::wstring_view text) { if (text.empty()) { return {}; } const int bytes = ::WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast(text.size()), nullptr, 0, nullptr, nullptr); std::string out(static_cast(bytes), '\0'); ::WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast(text.size()), out.data(), bytes, nullptr, nullptr); return out; } /// A path as Win32 takes it: `/` as `\`, and an absolute drive path given the `\\?\` prefix that /// lifts the 260-character limit. std::wstring win_path(std::string_view utf8) { std::wstring out = wide(utf8); for (wchar_t &unit : out) { if (unit == L'/') { unit = L'\\'; } } if (out.size() >= 3 && out[1] == L':' && out[2] == L'\\') { return L"\\\\?\\" + out; } return out; } std::string_view cause_of(DWORD error) { switch (error) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_INVALID_DRIVE: return "FileNotFound"; case ERROR_DIRECTORY: return "NotDir"; case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: return "AccessDenied"; case ERROR_DISK_FULL: case ERROR_HANDLE_DISK_FULL: return "NoSpaceLeft"; case ERROR_ALREADY_EXISTS: case ERROR_FILE_EXISTS: return "PathAlreadyExists"; case ERROR_DIR_NOT_EMPTY: return "DirNotEmpty"; case ERROR_FILENAME_EXCED_RANGE: return "NameTooLong"; case ERROR_NOT_SAME_DEVICE: return "CrossDevice"; default: return "Unexpected"; } } Failure failed(const std::string &path, bool directory) { return Failure::file_system(cause_of(::GetLastError()), path, directory); } /// Closes a handle when it goes out of scope. class Handle { public: explicit Handle(HANDLE handle) : handle_(handle) {} Handle(const Handle &) = delete; Handle &operator=(const Handle &) = delete; Handle(Handle &&) = delete; Handle &operator=(Handle &&) = delete; ~Handle() { if (valid()) { ::CloseHandle(handle_); } } [[nodiscard]] bool valid() const { return handle_ != INVALID_HANDLE_VALUE; } [[nodiscard]] HANDLE get() const { return handle_; } private: HANDLE handle_; }; std::string parent_of(const std::string &path) { const std::size_t slash = path.find_last_of("/\\"); if (slash == std::string::npos) { return "."; } return path.substr(0, slash); } std::string name_of(const std::string &path) { const std::size_t slash = path.find_last_of("/\\"); return slash == std::string::npos ? path : path.substr(slash + 1); } std::string temp_beside(const std::string &path) { static std::atomic counter{0}; return join(parent_of(path), "." + name_of(path) + "." + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(counter.fetch_add(1)) + ".tmp"); } Result write_all(HANDLE handle, std::string_view bytes, const std::string &path) { std::size_t done = 0; while (done < bytes.size()) { const auto chunk = static_cast( std::min(bytes.size() - done, static_cast(1) << 30U)); DWORD wrote = 0; if (::WriteFile(handle, bytes.data() + done, chunk, &wrote, nullptr) == 0) { return std::unexpected(failed(path, false)); } done += wrote; } return {}; } std::int64_t unix_ns(FILETIME time) { const std::uint64_t ticks = (static_cast(time.dwHighDateTime) << 32U) | time.dwLowDateTime; // 100 ns ticks since 1601-01-01; the Unix epoch is 11644473600 seconds later. return (static_cast(ticks) - 116444736000000000LL) * 100; } State state_of(DWORD attributes) { if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { return State::link; } if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { return State::directory; } return State::file; } } // namespace Result state(const std::string &path) { const DWORD attributes = ::GetFileAttributesW(win_path(path).c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { const DWORD error = ::GetLastError(); if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) { return State::missing; } return std::unexpected(Failure::file_system(cause_of(error), path, false)); } return state_of(attributes); } Result> read(const std::string &path, std::size_t limit) { const Handle file(::CreateFileW(win_path(path).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); if (!file.valid()) { const DWORD error = ::GetLastError(); if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) { return std::optional{}; } return std::unexpected(Failure::file_system(cause_of(error), path, false)); } std::string out; std::array buffer{}; while (true) { DWORD got = 0; if (::ReadFile(file.get(), buffer.data(), static_cast(buffer.size()), &got, nullptr) == 0) { return std::unexpected(failed(path, false)); } if (got == 0) { break; } out.append(buffer.data(), got); if (out.size() > limit) { return std::unexpected(Failure::file_system("StreamTooLong", path, false)); } } return std::optional{std::move(out)}; } // NOLINTNEXTLINE(misc-no-recursion): one level per missing parent directory. Result make_path(const std::string &path) { if (path.empty()) { return {}; } const DWORD attributes = ::GetFileAttributesW(win_path(path).c_str()); if (attributes != INVALID_FILE_ATTRIBUTES) { if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { return {}; } return std::unexpected(Failure::file_system("PathAlreadyExists", path, true)); } const std::string parent = parent_of(path); if (parent != path && parent != "." && parent.size() > 2) { if (auto made = make_path(parent); !made) { return made; } } if (::CreateDirectoryW(win_path(path).c_str(), nullptr) == 0 && ::GetLastError() != ERROR_ALREADY_EXISTS) { return std::unexpected(failed(path, true)); } return {}; } Result write_atomic(const std::string &path, std::string_view bytes) { if (auto made = make_path(parent_of(path)); !made) { return made; } const std::string temp = temp_beside(path); { const Handle file(::CreateFileW(win_path(temp).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); if (!file.valid()) { return std::unexpected(failed(temp, false)); } if (auto wrote = write_all(file.get(), bytes, temp); !wrote) { ::DeleteFileW(win_path(temp).c_str()); return wrote; } if (::FlushFileBuffers(file.get()) == 0) { const Failure failure = failed(temp, false); ::DeleteFileW(win_path(temp).c_str()); return std::unexpected(failure); } } if (::MoveFileExW(win_path(temp).c_str(), win_path(path).c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) { const Failure failure = failed(path, false); ::DeleteFileW(win_path(temp).c_str()); return std::unexpected(failure); } return {}; } Result make_dir(const std::string &path) { if (::CreateDirectoryW(win_path(path).c_str(), nullptr) == 0) { return std::unexpected(failed(path, true)); } return {}; } Result remove_file(const std::string &path) { if (::DeleteFileW(win_path(path).c_str()) == 0) { return std::unexpected(failed(path, false)); } return {}; } Result remove_dir(const std::string &path) { if (::RemoveDirectoryW(win_path(path).c_str()) == 0) { return std::unexpected(failed(path, true)); } return {}; } Result rename(const std::string &from, const std::string &to) { if (::MoveFileExW(win_path(from).c_str(), win_path(to).c_str(), 0) == 0) { return std::unexpected(failed(from, true)); } return {}; } Result copy_file(const std::string &from, const std::string &to) { if (::CopyFileW(win_path(from).c_str(), win_path(to).c_str(), FALSE) == 0) { return std::unexpected(failed(to, false)); } return {}; } Result real_path(const std::string &path) { const Handle file(::CreateFileW(win_path(path).c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); if (!file.valid()) { return std::unexpected(failed(path, true)); } std::wstring buffer(32 * 1024, L'\0'); const DWORD got = ::GetFinalPathNameByHandleW(file.get(), buffer.data(), static_cast(buffer.size()), 0); if (got == 0 || got >= buffer.size()) { return std::unexpected(failed(path, true)); } buffer.resize(got); if (buffer.starts_with(L"\\\\?\\UNC\\")) { buffer = L"\\\\" + buffer.substr(8); } else if (buffer.starts_with(L"\\\\?\\")) { buffer = buffer.substr(4); } return narrow(buffer); } Result> list(const std::string &path, bool follow) { if (!follow) { auto at = state(path); if (!at) { return std::unexpected(at.error()); } if (*at == State::link) { return std::unexpected(Failure::file_system("NotDir", path, true)); } } WIN32_FIND_DATAW found{}; const HANDLE search = ::FindFirstFileW((win_path(path) + L"\\*").c_str(), &found); if (search == INVALID_HANDLE_VALUE) { return std::unexpected(failed(path, true)); } std::vector out; do { const std::wstring_view name(static_cast(found.cFileName)); if (name == L"." || name == L"..") { continue; } out.push_back(Entry{.name = narrow(name), .kind = state_of(found.dwFileAttributes)}); } while (::FindNextFileW(search, &found) != 0); const DWORD error = ::GetLastError(); ::FindClose(search); if (error != ERROR_NO_MORE_FILES) { return std::unexpected(Failure::file_system(cause_of(error), path, true)); } return out; } Result stamp(const std::string &path) { WIN32_FILE_ATTRIBUTE_DATA data{}; if (::GetFileAttributesExW(win_path(path).c_str(), GetFileExInfoStandard, &data) == 0) { return std::unexpected(failed(path, false)); } const std::uint64_t size = (static_cast(data.nFileSizeHigh) << 32U) | data.nFileSizeLow; return Stamp{.size = size, .mtime_ns = unix_ns(data.ftLastWriteTime)}; } bool is_empty_directory(const std::string &path) { auto at = state(path); if (!at || *at != State::directory) { return false; } auto entries = list(path, false); return entries && entries->empty(); } std::optional read_link(const std::string &path) { const Handle file(::CreateFileW( win_path(path).c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr)); if (!file.valid()) { return std::nullopt; } std::vector buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); DWORD got = 0; if (::DeviceIoControl(file.get(), FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer.data(), static_cast(buffer.size()), &got, nullptr) == 0) { return std::nullopt; } const auto read16 = [&](std::size_t at) -> std::size_t { return at + 1 < got ? buffer.at(at) | (static_cast(buffer.at(at + 1)) << 8U) : 0; }; const std::uint32_t tag = static_cast(read16(0)) | (static_cast(read16(2)) << 16U); // A mount point's names start after 16 bytes, a symlink's after 20 (its flags field). std::size_t names = 0; if (tag == IO_REPARSE_TAG_MOUNT_POINT) { names = 16; } else if (tag == IO_REPARSE_TAG_SYMLINK) { names = 20; } else { return std::nullopt; } const std::size_t offset = read16(8); const std::size_t length = read16(10); if (names + offset + length > got) { return std::nullopt; } std::wstring target(length / 2, L'\0'); std::memcpy(target.data(), buffer.data() + names + offset, length); if (target.starts_with(L"\\??\\")) { target = target.substr(4); } return narrow(target); } namespace { /// Writes the junction's reparse point into the empty directory `link_w`. bool set_junction(const std::wstring &link_w, const std::string &target) { std::wstring print = wide(target); for (wchar_t &unit : print) { if (unit == L'/') { unit = L'\\'; } } const std::wstring substitute = L"\\??\\" + print; const auto data = junction_buffer(std::u16string(substitute.begin(), substitute.end()), std::u16string(print.begin(), print.end())); if (!data) { return false; } const Handle handle(::CreateFileW( link_w.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr)); if (!handle.valid()) { return false; } DWORD returned = 0; std::vector bytes = *data; return ::DeviceIoControl(handle.get(), FSCTL_SET_REPARSE_POINT, bytes.data(), static_cast(bytes.size()), nullptr, 0, &returned, nullptr) != 0; } } // namespace bool link_directory(const std::string &link, const std::string &target) { const std::wstring link_w = win_path(link); if (::CreateDirectoryW(link_w.c_str(), nullptr) == 0) { return false; } if (set_junction(link_w, target)) { return true; } // Our own empty directory; a leftover one is replaced on the next run. ::RemoveDirectoryW(link_w.c_str()); return false; } FileLock::FileLock(FileLock &&other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} FileLock &FileLock::operator=(FileLock &&other) noexcept { if (this != &other) { release(); handle_ = std::exchange(other.handle_, nullptr); } return *this; } FileLock::~FileLock() { release(); } void FileLock::release() noexcept { if (handle_ != nullptr) { ::CloseHandle(handle_); handle_ = nullptr; } } Result> FileLock::try_acquire(const std::string &path) { const HANDLE file = ::CreateFileW(win_path(path).c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return std::unexpected(failed(path, false)); } // The first byte, exclusive, as Zig's File.tryLock locks it, so an older build still excludes // this one. OVERLAPPED at{}; if (::LockFileEx(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &at) == 0) { const DWORD error = ::GetLastError(); ::CloseHandle(file); if (error == ERROR_LOCK_VIOLATION || error == ERROR_IO_PENDING) { return std::optional{}; } return std::unexpected(Failure::file_system(cause_of(error), path, false)); } FileLock lock; lock.handle_ = file; return std::optional{std::move(lock)}; } } // namespace packutils::fs #endif