// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include module Crafter.DB:File_impl; import :File; import :Error; import std; using namespace Crafter::DB; namespace fs = std::filesystem; namespace { std::error_code Errno() noexcept { return std::error_code(errno, std::generic_category()); } [[noreturn]] void Fail(std::string_view what, const fs::path& path) { throw IOError(what, Errno(), path); } } // --- File ------------------------------------------------------------------- File::File(File&& other) noexcept : descriptor(other.descriptor), path(std::move(other.path)) { other.descriptor = -1; } File& File::operator=(File&& other) noexcept { if (this != &other) { Close(); descriptor = other.descriptor; path = std::move(other.path); other.descriptor = -1; } return *this; } File::~File() { Close(); } File File::Open(const fs::path& path, bool writable) { File file; file.path = path; // O_NOFOLLOW: a private cache file must not be swapped for a symlink to // somewhere we would then happily write 0600 data into. const std::int32_t flags = (writable ? O_RDWR : O_RDONLY) | O_CLOEXEC | O_NOFOLLOW; file.descriptor = ::open(path.c_str(), flags); if (file.descriptor == -1) Fail("cannot open", path); return file; } File File::CreateExclusive(const fs::path& path, fs::perms permissions) { File file; file.path = path; file.descriptor = ::open(path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, static_cast(permissions)); if (file.descriptor == -1) Fail("cannot create", path); return file; } void File::LockExclusive() { if (::flock(descriptor, LOCK_EX | LOCK_NB) == -1) { if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string())); Fail("cannot lock", path); } } void File::LockShared() { if (::flock(descriptor, LOCK_SH | LOCK_NB) == -1) { if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string())); Fail("cannot lock", path); } } std::uint64_t File::Size() const { struct stat info{}; if (::fstat(descriptor, &info) == -1) Fail("cannot stat", path); return static_cast(info.st_size); } void File::Truncate(std::uint64_t length) { if (::ftruncate(descriptor, static_cast(length)) == -1) Fail("cannot truncate", path); } void File::Allocate(std::uint64_t offset, std::uint64_t length) { if (::fallocate(descriptor, 0, static_cast(offset), static_cast(length)) == 0) return; if (errno != EOPNOTSUPP && errno != ENOSYS) Fail("cannot allocate space", path); // Filesystems without fallocate (FUSE, 9p, exFAT): posix_fallocate falls // back to writing zeros, which equally guarantees the blocks exist. const std::int32_t result = ::posix_fallocate(descriptor, static_cast(offset), static_cast(length)); if (result != 0) throw IOError("cannot allocate space", std::error_code(result, std::generic_category()), path); } void File::ReadAt(std::uint64_t offset, std::span out) const { while (!out.empty()) { const ssize_t got = ::pread(descriptor, out.data(), out.size(), static_cast(offset)); if (got == -1) { if (errno == EINTR) continue; Fail("read failed", path); } if (got == 0) throw CorruptError(std::format("{}: file ends {} bytes short", path.string(), out.size())); out = out.subspan(static_cast(got)); offset += static_cast(got); } } void File::WriteAt(std::uint64_t offset, std::span in) { while (!in.empty()) { const ssize_t put = ::pwrite(descriptor, in.data(), in.size(), static_cast(offset)); if (put == -1) { if (errno == EINTR) continue; Fail("write failed", path); } in = in.subspan(static_cast(put)); offset += static_cast(put); } } void File::SyncData() { if (::fdatasync(descriptor) == -1) Fail("fdatasync failed", path); } void File::Sync() { if (::fsync(descriptor) == -1) Fail("fsync failed", path); } void File::Close() noexcept { if (descriptor != -1) { ::close(descriptor); descriptor = -1; } } // --- MappedRegion ----------------------------------------------------------- MappedRegion::MappedRegion(MappedRegion&& other) noexcept : base(other.base), reserved(other.reserved), mapped(other.mapped), dontDump(other.dontDump) { other.base = nullptr; other.reserved = 0; other.mapped = 0; } MappedRegion& MappedRegion::operator=(MappedRegion&& other) noexcept { if (this != &other) { Release(); base = other.base; reserved = other.reserved; mapped = other.mapped; dontDump = other.dontDump; other.base = nullptr; other.reserved = 0; other.mapped = 0; } return *this; } MappedRegion::~MappedRegion() { Release(); } void MappedRegion::Reserve(std::uint64_t bytes) { Release(); // PROT_NONE + MAP_NORESERVE costs no memory and no commit charge: it only // claims the addresses, so later MAP_FIXED calls cannot collide with // anything else the process maps. void* address = ::mmap(nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); if (address == MAP_FAILED) throw IOError(std::format("cannot reserve {} bytes of address space", bytes), Errno(), fs::path()); base = static_cast(address); reserved = bytes; mapped = 0; } void MappedRegion::Map(const File& file, std::uint64_t fileOffset, std::uint64_t length, bool writable) { if (mapped + length > reserved) { throw IOError(std::format("table needs more than the {} bytes of address space reserved per region; reopen with a larger OpenOptions::reserveBytes", reserved), std::make_error_code(std::errc::not_enough_memory), file.Path()); } void* address = ::mmap(base + mapped, length, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED | MAP_FIXED, file.Descriptor(), static_cast(fileOffset)); if (address == MAP_FAILED) Fail("cannot map", file.Path()); if (dontDump) ::madvise(address, length, MADV_DONTDUMP); mapped += length; } // MADV_DONTDUMP only. MADV_DONTFORK was considered and rejected: it turns any // touch of an inherited table in a forked child into SIGSEGV. The mapping is // MAP_SHARED, so a child reads consistently; writes are refused by the // owner-process check in TableFile. void MappedRegion::ExcludeFromCoreDumps() { dontDump = true; if (mapped != 0) ::madvise(base, mapped, MADV_DONTDUMP); } void MappedRegion::WillNeed(std::uint64_t length) { if (length != 0) ::madvise(base, std::min(length, mapped), MADV_WILLNEED); } void MappedRegion::Release() noexcept { if (base != nullptr) { ::munmap(base, reserved); base = nullptr; reserved = 0; mapped = 0; } } // --- MappedPages ------------------------------------------------------------ MappedPages::MappedPages(MappedPages&& other) noexcept : data(other.data), length(other.length) { other.data = nullptr; other.length = 0; } MappedPages& MappedPages::operator=(MappedPages&& other) noexcept { if (this != &other) { Release(); data = other.data; length = other.length; other.data = nullptr; other.length = 0; } return *this; } MappedPages::~MappedPages() { Release(); } void MappedPages::Map(const File& file, std::uint64_t fileOffset, std::uint64_t mapLength, bool writable) { Release(); void* address = ::mmap(nullptr, mapLength, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, file.Descriptor(), static_cast(fileOffset)); if (address == MAP_FAILED) Fail("cannot map header", file.Path()); data = static_cast(address); length = mapLength; } void MappedPages::Sync() { if (data != nullptr && ::msync(data, length, MS_SYNC) == -1) throw IOError("msync failed", Errno(), fs::path()); } void MappedPages::Release() noexcept { if (data != nullptr) { ::munmap(data, length); data = nullptr; length = 0; } } // --- Os --------------------------------------------------------------------- void Os::CreateDirectories(const fs::path& directory, fs::perms permissions) { if (directory.empty()) return; fs::path current; for (const fs::path& part : directory) { current /= part; if (current == directory.root_path() || current.empty()) continue; if (::mkdir(current.c_str(), static_cast(permissions)) == -1 && errno != EEXIST) Fail("cannot create directory", current); } } void Os::Rename(const fs::path& from, const fs::path& to) { if (::rename(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot rename to {}", to.string()), from); } bool Os::RenameNoReplace(const fs::path& from, const fs::path& to) { if (::renameat2(AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE) == 0) return true; if (errno == EEXIST) return false; Fail(std::format("cannot rename to {}", to.string()), from); } void Os::HardLink(const fs::path& from, const fs::path& to) { if (::link(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot link to {}", to.string()), from); } bool Os::Remove(const fs::path& path) noexcept { return ::unlink(path.c_str()) == 0; } void Os::SyncDirectory(const fs::path& directory) { const fs::path dir = directory.empty() ? fs::path(".") : directory; const std::int32_t fd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (fd == -1) Fail("cannot open directory", dir); const std::int32_t result = ::fsync(fd); ::close(fd); if (result == -1) Fail("fsync of directory failed", dir); } void Os::RandomBytes(std::span out) { std::random_device device; for (std::size_t i = 0; i < out.size(); i += 4) { const std::uint32_t value = device(); std::memcpy(out.data() + i, &value, std::min(4, out.size() - i)); } } std::uint64_t Os::UnixNanos() noexcept { return static_cast(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); } std::uint64_t Os::PageSize() noexcept { const long size = ::sysconf(_SC_PAGESIZE); return size > 0 ? static_cast(size) : 4096; } std::int64_t Os::ProcessId() noexcept { return static_cast(::getpid()); }