Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457// fs.hpp on Linux and macOS, over POSIX calls. AI-authored (Claude).#ifndef _WIN32
#include "fs.hpp"
#include <array>#include <atomic>#include <cerrno>#include <climits>#include <cstddef>#include <cstdint>#include <cstdlib>#include <dirent.h>#include <fcntl.h>#include <optional>#include <string>#include <string_view>#include <sys/file.h>#include <sys/stat.h>#include <unistd.h>#include <utility>#include <vector>
namespace packutils::fs {
namespace {
/// The name Zig's std gives the error, which the host's .NET-shaped messages key on.std::string_view cause_of(int error) { switch (error) { case ENOENT: return "FileNotFound"; case ENOTDIR: return "NotDir"; case EACCES: case EPERM: return "AccessDenied"; case EISDIR: return "IsDir"; case ENOSPC: return "NoSpaceLeft"; case EEXIST: return "PathAlreadyExists"; case ENOTEMPTY: return "DirNotEmpty"; case ELOOP: return "SymLinkLoop"; case EBUSY: return "FileBusy"; case EROFS: return "ReadOnlyFileSystem"; case ENAMETOOLONG: return "NameTooLong"; case EXDEV: return "CrossDevice"; default: return "Unexpected"; }}
/// `open`, which is variadic for its mode.int open_file(const std::string &path, int flags, mode_t mode = 0) { // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg): POSIX open takes its mode as a vararg. return ::open(path.c_str(), flags, mode);}
Failure failed(const std::string &path, bool directory) { return Failure::file_system(cause_of(errno), path, directory);}
/// Closes a descriptor when it goes out of scope.class Descriptor { public: explicit Descriptor(int fd) : fd_(fd) {} Descriptor(const Descriptor &) = delete; Descriptor &operator=(const Descriptor &) = delete; Descriptor(Descriptor &&) = delete; Descriptor &operator=(Descriptor &&) = delete; ~Descriptor() { if (fd_ >= 0) { ::close(fd_); } } [[nodiscard]] int get() const { return fd_; }
private: int fd_;};
std::string parent_of(const std::string &path) { const std::size_t slash = path.find_last_of('/'); if (slash == std::string::npos) { return "."; } return slash == 0 ? std::string("/") : 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);}
/// `.<name>.<pid>-<n>.tmp` beside `path`: unique within this process and across processes.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(::getpid()) + "-" + std::to_string(counter.fetch_add(1)) + ".tmp");}
Result<void> write_all(int fd, std::string_view bytes, const std::string &path) { std::size_t done = 0; while (done < bytes.size()) { const std::string_view rest = bytes.substr(done); const ssize_t wrote = ::write(fd, rest.data(), rest.size()); if (wrote < 0) { if (errno == EINTR) { continue; } return std::unexpected(failed(path, false)); } done += static_cast<std::size_t>(wrote); } return {};}
State state_of(mode_t mode) { if (S_ISLNK(mode)) { return State::link; } if (S_ISDIR(mode)) { return State::directory; } if (S_ISREG(mode)) { return State::file; } return State::other;}
std::int64_t mtime_ns(const struct stat &info) {#ifdef __APPLE__ const auto &time = info.st_mtimespec;#else const auto &time = info.st_mtim;#endif return (static_cast<std::int64_t>(time.tv_sec) * 1'000'000'000) + time.tv_nsec;}
} // namespace
Result<State> state(const std::string &path) { struct stat info{}; if (::lstat(path.c_str(), &info) != 0) { if (errno == ENOENT) { return State::missing; } return std::unexpected(failed(path, false)); } return state_of(info.st_mode);}
Result<std::optional<std::string>> read(const std::string &path, std::size_t limit) { const Descriptor file(open_file(path, O_RDONLY | O_CLOEXEC)); if (file.get() < 0) { if (errno == ENOENT) { return std::optional<std::string>{}; } return std::unexpected(failed(path, false)); } std::string out; std::array<char, std::size_t{64} * 1024> buffer{}; while (true) { const ssize_t got = ::read(file.get(), buffer.data(), buffer.size()); if (got < 0) { if (errno == EINTR) { continue; } return std::unexpected(failed(path, false)); } if (got == 0) { break; } out.append(buffer.data(), static_cast<std::size_t>(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 {}; } struct stat info{}; if (::stat(path.c_str(), &info) == 0) { if (S_ISDIR(info.st_mode)) { return {}; } return std::unexpected(Failure::file_system("PathAlreadyExists", path, true)); } const std::string parent = parent_of(path); if (parent != path && parent != "." && parent != "/") { if (auto made = make_path(parent); !made) { return made; } } if (::mkdir(path.c_str(), 0777) != 0 && errno != EEXIST) { 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 Descriptor file(open_file(temp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0666)); if (file.get() < 0) { return std::unexpected(failed(temp, false)); } if (auto wrote = write_all(file.get(), bytes, temp); !wrote) { ::unlink(temp.c_str()); return wrote; } if (::fsync(file.get()) != 0) { const Failure failure = failed(temp, false); ::unlink(temp.c_str()); return std::unexpected(failure); } } if (::rename(temp.c_str(), path.c_str()) != 0) { const Failure failure = failed(path, false); ::unlink(temp.c_str()); return std::unexpected(failure); } return {};}
Result<void> make_dir(const std::string &path) { if (::mkdir(path.c_str(), 0777) != 0) { return std::unexpected(failed(path, true)); } return {};}
Result<void> remove_file(const std::string &path) { if (::unlink(path.c_str()) != 0) { return std::unexpected(failed(path, false)); } return {};}
Result<void> remove_dir(const std::string &path) { if (::rmdir(path.c_str()) != 0) { return std::unexpected(failed(path, true)); } return {};}
Result<void> rename(const std::string &from, const std::string &to) { if (::rename(from.c_str(), to.c_str()) != 0) { return std::unexpected(failed(from, true)); } return {};}
Result<void> copy_file(const std::string &from, const std::string &to) { const Descriptor source(open_file(from, O_RDONLY | O_CLOEXEC)); if (source.get() < 0) { return std::unexpected(failed(from, false)); } struct stat info{}; if (::fstat(source.get(), &info) != 0) { return std::unexpected(failed(from, false)); } const std::string temp = temp_beside(to); { const Descriptor target( open_file(temp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, info.st_mode & 07777U)); if (target.get() < 0) { return std::unexpected(failed(to, false)); } std::array<char, std::size_t{64} * 1024> buffer{}; while (true) { const ssize_t got = ::read(source.get(), buffer.data(), buffer.size()); if (got < 0) { if (errno == EINTR) { continue; } const Failure failure = failed(from, false); ::unlink(temp.c_str()); return std::unexpected(failure); } if (got == 0) { break; } if (auto wrote = write_all(target.get(), std::string_view(buffer.data(), static_cast<std::size_t>(got)), to); !wrote) { ::unlink(temp.c_str()); return wrote; } } } if (::rename(temp.c_str(), to.c_str()) != 0) { const Failure failure = failed(to, false); ::unlink(temp.c_str()); return std::unexpected(failure); } return {};}
Result<std::string> real_path(const std::string &path) { std::array<char, PATH_MAX> buffer{}; if (::realpath(path.c_str(), buffer.data()) == nullptr) { return std::unexpected(failed(path, true)); } return std::string(buffer.data());}
Result<std::vector<Entry>> list(const std::string &path, bool follow) { const int flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC | (follow ? 0 : O_NOFOLLOW); const int fd = open_file(path, flags); if (fd < 0) { return std::unexpected(failed(path, true)); } DIR *dir = ::fdopendir(fd); if (dir == nullptr) { const Failure failure = failed(path, true); ::close(fd); return std::unexpected(failure); } std::vector<Entry> out; while (true) { errno = 0; const dirent *item = ::readdir(dir); if (item == nullptr) { if (errno != 0) { const Failure failure = failed(path, true); ::closedir(dir); return std::unexpected(failure); } break; } const std::string_view name(static_cast<const char *>(item->d_name)); if (name == "." || name == "..") { continue; } State kind = State::other; switch (item->d_type) { case DT_DIR: kind = State::directory; break; case DT_REG: kind = State::file; break; case DT_LNK: kind = State::link; break; case DT_UNKNOWN: { struct stat info{}; if (::fstatat(::dirfd(dir), std::string(name).c_str(), &info, AT_SYMLINK_NOFOLLOW) != 0) { const Failure failure = failed(join(path, name), false); ::closedir(dir); return std::unexpected(failure); } kind = state_of(info.st_mode); break; } default: kind = State::other; } out.push_back(Entry{.name = std::string(name), .kind = kind}); } ::closedir(dir); return out;}
Result<Stamp> stamp(const std::string &path) { struct stat info{}; if (::lstat(path.c_str(), &info) != 0) { return std::unexpected(failed(path, false)); } return Stamp{.size = static_cast<std::uint64_t>(info.st_size), .mtime_ns = mtime_ns(info)};}
bool is_empty_directory(const std::string &path) { auto entries = list(path, false); return entries && entries->empty();}
std::optional<std::string> read_link(const std::string &path) { std::array<char, PATH_MAX> buffer{}; const ssize_t got = ::readlink(path.c_str(), buffer.data(), buffer.size()); if (got < 0) { return std::nullopt; } return std::string(buffer.data(), static_cast<std::size_t>(got));}
bool link_directory(const std::string &link, const std::string &target) { return ::symlink(target.c_str(), link.c_str()) == 0;}
FileLock::FileLock(FileLock &&other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
FileLock &FileLock::operator=(FileLock &&other) noexcept { if (this != &other) { release(); fd_ = std::exchange(other.fd_, -1); } return *this;}
FileLock::~FileLock() { release();}
void FileLock::release() noexcept { if (fd_ >= 0) { ::close(fd_); fd_ = -1; }}
Result<std::optional<FileLock>> FileLock::try_acquire(const std::string &path) { // flock, as Zig's File.tryLock takes it, so an older build's lock still excludes this one. const int fd = open_file(path, O_RDWR | O_CREAT | O_CLOEXEC, 0666); if (fd < 0) { return std::unexpected(failed(path, false)); } while (::flock(fd, LOCK_EX | LOCK_NB) != 0) { if (errno == EINTR) { continue; } const int error = errno; ::close(fd); if (error == EWOULDBLOCK) { return std::optional<FileLock>{}; } errno = error; return std::unexpected(failed(path, false)); } FileLock lock; lock.fd_ = fd; return std::optional<FileLock>{std::move(lock)};}
} // namespace packutils::fs
#endif