// fs.hpp on Linux and macOS, over POSIX calls. AI-authored (Claude). #ifndef _WIN32 #include "fs.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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); } /// `..-.tmp` beside `path`: unique within this process and across processes. std::string temp_beside(const std::string &path) { static std::atomic counter{0}; return join(parent_of(path), "." + name_of(path) + "." + std::to_string(::getpid()) + "-" + std::to_string(counter.fetch_add(1)) + ".tmp"); } Result 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(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(time.tv_sec) * 1'000'000'000) + time.tv_nsec; } } // namespace Result 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> 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{}; } return std::unexpected(failed(path, false)); } std::string out; std::array 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(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 {}; } 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 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 make_dir(const std::string &path) { if (::mkdir(path.c_str(), 0777) != 0) { return std::unexpected(failed(path, true)); } return {}; } Result remove_file(const std::string &path) { if (::unlink(path.c_str()) != 0) { return std::unexpected(failed(path, false)); } return {}; } Result remove_dir(const std::string &path) { if (::rmdir(path.c_str()) != 0) { return std::unexpected(failed(path, true)); } return {}; } Result 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 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 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(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 real_path(const std::string &path) { std::array buffer{}; if (::realpath(path.c_str(), buffer.data()) == nullptr) { return std::unexpected(failed(path, true)); } return std::string(buffer.data()); } Result> 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 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(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(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(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 read_link(const std::string &path) { std::array 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(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> 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{}; } errno = error; return std::unexpected(failed(path, false)); } FileLock lock; lock.fd_ = fd; return std::optional{std::move(lock)}; } } // namespace packutils::fs #endif