Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509// 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 <algorithm>#include <array>#include <atomic>#include <cstddef>#include <cstdint>#include <cstring>#include <optional>#include <string>#include <string_view>#include <utility>#include <vector>
#include <windows.h>#include <winioctl.h>
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<int>(utf8.size()), nullptr, 0); std::wstring out(static_cast<std::size_t>(units), L'\0'); ::MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(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<int>(text.size()), nullptr, 0, nullptr, nullptr); std::string out(static_cast<std::size_t>(bytes), '\0'); ::WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast<int>(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<std::uint64_t> counter{0}; return join(parent_of(path), "." + name_of(path) + "." + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(counter.fetch_add(1)) + ".tmp");}
Result<void> 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<DWORD>( std::min<std::size_t>(bytes.size() - done, static_cast<std::size_t>(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<std::uint64_t>(time.dwHighDateTime) << 32U) | time.dwLowDateTime; // 100 ns ticks since 1601-01-01; the Unix epoch is 11644473600 seconds later. return (static_cast<std::int64_t>(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> 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<std::optional<std::string>> 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<std::string>{}; } return std::unexpected(Failure::file_system(cause_of(error), path, false)); } std::string out; std::array<char, 64 * 1024> buffer{}; while (true) { DWORD got = 0; if (::ReadFile(file.get(), buffer.data(), static_cast<DWORD>(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::string>{std::move(out)};}
// NOLINTNEXTLINE(misc-no-recursion): one level per missing parent directory.Result<void> 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<void> 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<void> make_dir(const std::string &path) { if (::CreateDirectoryW(win_path(path).c_str(), nullptr) == 0) { return std::unexpected(failed(path, true)); } return {};}
Result<void> remove_file(const std::string &path) { if (::DeleteFileW(win_path(path).c_str()) == 0) { return std::unexpected(failed(path, false)); } return {};}
Result<void> remove_dir(const std::string &path) { if (::RemoveDirectoryW(win_path(path).c_str()) == 0) { return std::unexpected(failed(path, true)); } return {};}
Result<void> 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<void> 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<std::string> 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<DWORD>(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<std::vector<Entry>> 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<Entry> out; do { const std::wstring_view name(static_cast<const wchar_t *>(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> 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<std::uint64_t>(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<std::string> 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<std::uint8_t> buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); DWORD got = 0; if (::DeviceIoControl(file.get(), FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer.data(), static_cast<DWORD>(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<std::size_t>(buffer.at(at + 1)) << 8U) : 0; }; const std::uint32_t tag = static_cast<std::uint32_t>(read16(0)) | (static_cast<std::uint32_t>(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<std::uint8_t> bytes = *data; return ::DeviceIoControl(handle.get(), FSCTL_SET_REPARSE_POINT, bytes.data(), static_cast<DWORD>(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<std::optional<FileLock>> 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<FileLock>{}; } return std::unexpected(Failure::file_system(cause_of(error), path, false)); } FileLock lock; lock.handle_ = file; return std::optional<FileLock>{std::move(lock)};}
} // namespace packutils::fs
#endif