Crafter.DB/implementations/Crafter.DB-File.cpp

317 lines
11 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#define _GNU_SOURCE 1
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
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<mode_t>(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<std::uint64_t>(info.st_size);
}
void File::Truncate(std::uint64_t length) {
if (::ftruncate(descriptor, static_cast<off_t>(length)) == -1) Fail("cannot truncate", path);
}
void File::Allocate(std::uint64_t offset, std::uint64_t length) {
if (::fallocate(descriptor, 0, static_cast<off_t>(offset), static_cast<off_t>(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<off_t>(offset), static_cast<off_t>(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<std::byte> out) const {
while (!out.empty()) {
const ssize_t got = ::pread(descriptor, out.data(), out.size(), static_cast<off_t>(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<std::size_t>(got));
offset += static_cast<std::uint64_t>(got);
}
}
void File::WriteAt(std::uint64_t offset, std::span<const std::byte> in) {
while (!in.empty()) {
const ssize_t put = ::pwrite(descriptor, in.data(), in.size(), static_cast<off_t>(offset));
if (put == -1) {
if (errno == EINTR) continue;
Fail("write failed", path);
}
in = in.subspan(static_cast<std::size_t>(put));
offset += static_cast<std::uint64_t>(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<std::byte*>(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<off_t>(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<off_t>(fileOffset));
if (address == MAP_FAILED) Fail("cannot map header", file.Path());
data = static_cast<std::byte*>(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<mode_t>(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<std::byte> 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<std::size_t>(4, out.size() - i));
}
}
std::uint64_t Os::UnixNanos() noexcept {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(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<std::uint64_t>(size) : 4096;
}
std::int64_t Os::ProcessId() noexcept {
return static_cast<std::int64_t>(::getpid());
}