ona/source/turtle.cpp
kayomn 261b62fc2d
Some checks failed
continuous-integration/drone/push Build is failing
Replace app library with Turtle OS abstraction
2023-02-28 23:30:14 +00:00

605 lines
18 KiB
C++
Executable File

module;
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
export module turtle;
import coral;
import coral.files;
import coral.io;
using coral::closure;
using coral::io_error;
using coral::path;
using coral::slice;
using coral::unreachable;
using coral::usize;
export namespace turtle {
/**
* Path to a native I/O resource that is big enough for every supported platform.
*/
struct native_path {
/**
* Errors that may occur during a path joining operation.
*
* [join_error::overflow] signals that the given path join exceeds the maximum valid length of a native path.
*/
enum class join_error {
overflow,
};
/**
* Maximum number of bytes in a native path.
*/
static usize const max = 4095;
native_path() = default;
/**
* Constructs a native path from `text`, raising a static assertion if it is larger than [max].
*/
template<usize text_size> constexpr native_path(char const(&text)[text_size]) {
static_assert(text_size <= max);
for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i];
this->buffer[text_size] = 0;
}
/**
* Returns a weak reference to the native path as a [coral::slice].
*
* *Note*: this is an `O(N)` time function, where `N` is the path length.
*/
constexpr slice<char const> as_slice() const {
return {this->buffer, this->filled()};
}
/**
* Returns the number of bytes composing the native path.
*
* *Note*: this is an `O(N)` time function, where `N` is the path length.
*/
constexpr usize filled() const {
usize length {0};
while (this->buffer[length]) length += 1;
return length;
}
/**
* Attempts to create a new native path from the current native path joined with `text`, returning it or a
* [join_error].
*
* *Note*: this is an `O(N)` time function, where `N` is the path length.
*/
constexpr coral::expected<native_path, join_error> joined(slice<char const> const & text) const {
usize buffer_filled {this->filled()};
if (text.length >= (max - buffer_filled)) return join_error::overflow;
native_path joined_path {*this};
for (char const c : text) {
joined_path.buffer[buffer_filled] = c;
buffer_filled += 1;
}
return joined_path;
}
private:
char buffer[max + 1] {0};
};
/**
* Results from a native I/O resource closing operation.
*
* [close_result::ok] means that no errors occured and the resource has been successfully closed if it was
* open.
*
* [close_result::io_unavailable] is a generic error to communicate that something between the hardware and the
* operating system layer failed while closing the resource.
*
* [close_result::access_denied] reports that the process does not have the required permissions to open the
* resource. This is a rare but possible error that is only possible by a system changing the underlying
* resource permissions after the native resource has already opened it.
*/
enum class [[nodiscard]] close_result {
ok,
io_unavailable,
access_denied,
};
/**
* Results from a native I/O resource opening operation.
*
* [open_result::ok] means that no errors occured and the resource has been successfully opened.
*
* [open_result::io_unavailable] is a generic error to communicate that something between the hardware and the
* operating system layer failed while opening the resource.
*
* [open_result::access_denied] reports that the process does not have the required permissions to open the
* resource. While all platforms implement some form of file permissions, the specifics of this error are
* opaque to the caller.
*
* [open_result::not_found] indicates that no resource matching the opening query was found.
*
* [open_result::too_many] signals that there are too many files open in the current process and / or the
* wider operating system at the moment to open the resource.
*
* [open_result::too_big] signals that the resource is too big to open. The usual cause of this is error is
* attempting to open a file bigger than the addressable file range supported by the compiled application.
*
* [open_result::out_of_memory] signals that the system does not have enough memory remaining to open the resource.
*/
enum class [[nodiscard]] open_result {
ok,
io_unavailable,
access_denied,
not_found,
too_many,
too_big,
out_of_memory,
};
/**
* Maps a [close_result] to the equivalent [open_result] and returns the value.
*/
open_result close_to_open_result(close_result result) {
switch (result) {
case close_result::ok: return open_result::ok;
case close_result::io_unavailable: return open_result::io_unavailable;
case close_result::access_denied: return open_result::access_denied;
default: unreachable();
}
}
/**
* Provides unmanaged access to a native file.
*/
struct native_file final : public coral::file_reader, public coral::file_writer {
/**
* Opening modes for files.
*
* [open_mode::read_only] requests read-only access to an existing file. Specifying a path to an invalid file
* will result in the open request failing.
*
* [open_mode::overwrite] requests write-only access to a new file. Specifying a path to an existing file will
* result in it being wiped and overwritten (assuming it is not protected by the underlying operating system).
*
* [open_mode::append] requests write-only access to an existing file. Specifying a path to an invalid file
* will result in the open request failing. The write cursor begins at the end of any existing data in the file.
*/
enum class open_mode {
read_only,
overwrite,
append,
};
native_file() = default;
/**
* Attempts to close any currently open file.
*
* A [close_result] is returned containing either [close_result::ok] to indicate success or any other value to
* indicate an error. See [close_result] for more details.
*
* *Note*: failing to close should not be treated as a reason to retry the closing operation, and should instead
* be used to inform the end-user that the operation failed or that the process should exit.
*/
close_result close() {
errno = 0;
if (::close(this->fd) != 0) switch (errno) {
case EINTR: case EIO: case ENOSPC: return close_result::io_unavailable;
case EDQUOT: return close_result::access_denied;
default: unreachable();
}
this->fd = 0;
return close_result::ok;
}
/**
* Returns `true` if a file is currently open, otherwise `false`.
*/
bool is_open() const {
return this->fd > 0;
}
/**
* Attempts to open a native file at `file_path` using `file_open_mode` for the access policy. See [open_mode]
* for more information on how files may be opened.
*
* An [open_result] is returned containing either [open_result::ok] to indicate success or any other value to
* indicate an error. See [open_result] for more details.
*
* *Note*: the opened file must be closed using [close] once no longer needed or the process will leak file
* handles.
*
* *Note*: it is recommended to prefer performing file I/O via [sandboxed_fs] unless direct file access is
* required.
*/
open_result open(native_path const & file_path, open_mode file_open_mode) {
if (this->is_open()) {
open_result const result {close_to_open_result(this->close())};
if (result != open_result::ok) return result;
}
constexpr int perms {S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH};
errno = 0;
switch (file_open_mode) {
case open_mode::read_only: {
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_RDONLY, perms);
break;
}
case open_mode::overwrite: {
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_CREAT | O_TRUNC, perms);
break;
}
case open_mode::append: {
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_APPEND | O_CREAT, perms);
break;
}
default: unreachable();
}
if (!this->is_open()) switch (errno) {
case EACCES: case EDQUOT: case ENXIO: case EPERM:
case EROFS: case ETXTBSY: return open_result::access_denied;
case EINTR: case ENOSPC: return open_result::io_unavailable;
case EINVAL: case EISDIR: case ELOOP: case ENAMETOOLONG:
case ENOENT: case ENOTDIR: return open_result::not_found;
case EOVERFLOW: case EFBIG: return open_result::too_big;
case EMFILE: case ENFILE: return open_result::too_many;
case ENOMEM: return open_result::out_of_memory;
default: unreachable();
}
return open_result::ok;
}
/**
* See [coral::file_reader::read].
*/
coral::expected<usize, io_error> read(slice<coral::u8> const & data) override {
if (!this->is_open()) return io_error::unavailable;
coral::size const data_read {::read(this->fd, data.pointer, sizeof(coral::u8) * data.length)};
if (data_read < 0) return io_error::unavailable;
return data_read;
}
/**
* See [coral::file_reader::seek] and [coral::file_writer::seek].
*/
coral::expected<coral::u64, io_error> seek(coral::u64 offset) override {
if (!this->is_open()) return io_error::unavailable;
coral::i64 const data_position {::lseek(this->fd,
static_cast<coral::i64>(coral::min(offset, coral::i64_max)), SEEK_SET)};
if (data_position == -1) return io_error::unavailable;
return static_cast<coral::u64>(data_position);
}
/**
* See [coral::file_reader::tell] and [coral::file_writer::tell].
*/
coral::expected<coral::u64, io_error> tell() override {
if (!this->is_open()) return io_error::unavailable;
coral::i64 const data_position {::lseek(this->fd, 0, SEEK_SET)};
if (data_position == -1) return io_error::unavailable;
return static_cast<coral::u64>(data_position);
}
/**
* See [coral::file_writer::write].
*/
coral::expected<usize, io_error> write(slice<coral::u8 const> const & data) override {
if (!this->is_open()) return io_error::unavailable;
coral::size const data_written {::write(this->fd, data.pointer, sizeof(coral::u8) * data.length)};
if (data_written < 0) return io_error::unavailable;
return data_written;
}
private:
int fd {0};
};
/**
* Provides unmanaged access to a native directory.
*/
struct native_directory final : public coral::file_walker {
native_directory() = default;
/**
* Attempts to close any currently open directory.
*
* A [close_result] is returned containing either [close_result::ok] to indicate success or any other value to
* indicate an error. See [close_result] for more details.
*
* *Note*: failing to close should not be treated as a reason to retry the closing operation, and should instead
* be used to inform the end-user that the operation failed or that the process should exit.
*/
close_result close() {
if (closedir(this->dir) == 0) return close_result::io_unavailable;
return close_result::ok;
}
/**
* See [coral::file_walker::has_next].
*/
bool has_next() override {
return this->entry != nullptr;
}
/**
* Returns `true` if a directory is currently open, otherwise `false`.
*/
bool is_open() const {
return this->dir != nullptr;
}
/**
* See [coral::file_walker::next].
*/
coral::expected<path, io_error> next() override {
usize name_length {0};
constexpr usize name_max {sizeof(dirent::d_name) / sizeof(char)};
while ((name_length < name_max) && (this->entry->d_name[name_length] != 0)) name_length += 1;
path current_path {path{}.joined(slice{this->entry->d_name, name_length})};
errno = 0;
this->entry = readdir(this->dir);
if (this->entry == nullptr) switch (errno) {
case EBADF: return io_error::unavailable;
default: unreachable();
}
return current_path;
}
/**
* Attempts to open a native directory at `directory_path`.
*
* An [open_result] is returned containing either [open_result::ok] to indicate success or any other value to
* indicate an error. See [open_result] for more details.
*
* *Note*: the opened directory must be closed using [close] once no longer needed or the process will leak
* directory streams.
*
* *Note*: if a directory is currently open under the native directory, it will attempt to close it before
* proceeding with opening the next. This means that [open] is safe to call without first calling [close].
*
* *Note*: it is recommended to prefer performing file I/O via [sandboxed_fs] unless direct file access is
* required.
*/
open_result open(native_path const & directory_path) {
if (this->is_open()) {
open_result const result {close_to_open_result(this->close())};
if (result != open_result::ok) return result;
}
// No room for zero terminator.
errno = 0;
this->dir = opendir(directory_path.as_slice().as_chars().pointer);
if (!this->is_open()) switch (errno) {
case EACCES: return open_result::access_denied;
case EMFILE: case ENFILE: return open_result::too_many;
case ENOENT: case ENOTDIR: return open_result::not_found;
case ENOMEM: return open_result::out_of_memory;
}
errno = 0;
this->entry = readdir(this->dir);
if (this->entry == nullptr) switch (errno) {
case EBADF: {
if (this->is_open()) {
open_result const result {close_to_open_result(this->close())};
if (result != open_result::ok) return result;
}
return open_result::io_unavailable;
}
default: unreachable();
}
return open_result::ok;
}
private:
DIR * dir {nullptr};
dirent * entry {nullptr};
};
/**
* [coral::fs] wrapper around native file system access to provide a managed and system-agnostic environment for
* performing file I/O.
*/
struct sandboxed_fs : public coral::fs {
/**
* Permission flags that a [sandboxed_fs] may specify for restricting access to it.
*/
struct permissions {
bool can_read;
bool can_write;
bool can_walk;
};
/**
* Constructs a sandbox located at `sandbox_path` with `sandbox_permissions` as the permissions given to users
* of it.
*/
sandboxed_fs(native_path const & sandbox_path, permissions const & access_permissions) {
this->path = sandbox_path;
this->access_permissions = access_permissions;
}
/**
* Returns a reference to a [sandboxed_fs] that provides access to the base directory which, on most systems, is
* the current working directory.
*
* The base directory may be used to access things loose files created outside of the application, such as user-
* generated files and modifications.
*
* *Note*: The base file system does not permit being written to.
*/
static sandboxed_fs & base() {
static sandboxed_fs base_fs {"./", {
.can_read = true,
.can_walk = true
}};
return base_fs;
}
/**
* Returns a reference to a [sandboxed_fs] that operates as a temporary file store.
*
* As the name implies, the existence of files that exist here are not guaranteed beyond the duration of an
* application run lifetime. The purpose of the temporary file store is to support transactional I/O
* operations that only want to replace files if new ones has been successfully constructed already.
*
* *Note*: The temp file system does not permit being walked.
*/
static sandboxed_fs & temp() {
static sandboxed_fs base_fs {"/tmp", {
.can_read = true,
.can_write = false,
}};
return base_fs;
}
/**
* See [coral::fs::read_file].
*
* *Note*: this function will only work on sandboxes with the [permissions::can_read] flag enabled.
*/
void read_file(path const & target_path, closure<void(coral::file_reader &)> const & then) override {
if (!this->access_permissions.can_read) return;
this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_file_path) -> void {
native_file file;
if (file.open(native_file_path, native_file::open_mode::read_only) != open_result::ok) return;
then(file);
// TODO: Error orphaned file handle!
if (file.close() != close_result::ok) return;
});
}
/**
* See [coral::fs::walk_files].
*
* *Note*: this function will only work on sandboxes with the [permissions::can_walk] flag enabled.
*/
void walk_files(path const & target_path, closure<void(coral::file_walker &)> const & then) override {
if (!this->access_permissions.can_walk) return;
this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_directory_path) -> void {
native_directory directory;
if (directory.open(native_directory_path) == open_result::ok) return;
then(directory);
// TODO: Error orphaned file handle!
if (directory.close() != close_result::ok) return;
});
}
/**
* See [coral::fs::write_file].
*
* *Note*: this function will only work on sandboxes with the [permissions::can_write] flag enabled.
*/
void write_file(path const & target_path, closure<void(coral::file_writer &)> const & then) override {
if (!this->access_permissions.can_write) return;
this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_file_path) -> void {
native_file file;
if (file.open(native_file_path, native_file::open_mode::overwrite) != open_result::ok) return;
then(file);
// TODO: Error orphaned file handle!
if (file.close() != close_result::ok) return;
});
}
private:
native_path path;
permissions access_permissions {
.can_read = false,
.can_write = false,
.can_walk = false,
};
};
/**
* Returns a reference to the process-wide output device used for writing data out from to the wider system.
*
* This [coral::writer] is particularly useful for command-line tools which require communicating with another
* process via pipes or an end-user via the shell.
*/
coral::writer & output() {
static struct : public coral::writer {
coral::expected<usize, io_error> write(slice<coral::u8 const> const & data) override {
coral::size const data_written {::write(STDOUT_FILENO, data.pointer, sizeof(coral::u8) * data.length)};
if (data_written == -1) return io_error::unavailable;
return data_written;
};
} output_writer;
return output_writer;
}
}