Replace app library with Turtle OS abstraction
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
3e413ea45d
commit
261b62fc2d
2
build.py
2
build.py
@ -30,6 +30,6 @@ def compile_package(root_module_name: str) -> None:
|
||||
|
||||
compile_package("coral")
|
||||
compile_package("oar")
|
||||
compile_package("app")
|
||||
compile_package("turtle")
|
||||
compile_package("runtime")
|
||||
subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True)
|
||||
|
317
source/app.cpp
317
source/app.cpp
@ -1,317 +0,0 @@
|
||||
module;
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
export module app;
|
||||
|
||||
import coral;
|
||||
import coral.files;
|
||||
import coral.image;
|
||||
import coral.io;
|
||||
import coral.math;
|
||||
|
||||
import oar;
|
||||
|
||||
using native_path = coral::fixed_buffer<4096>;
|
||||
|
||||
struct native_file : public coral::file_reader, public coral::file_writer {
|
||||
enum class open_mode {
|
||||
read_only,
|
||||
overwrite,
|
||||
};
|
||||
|
||||
enum class [[nodiscard]] close_result {
|
||||
ok,
|
||||
io_unavailable,
|
||||
};
|
||||
|
||||
enum class [[nodiscard]] open_result {
|
||||
ok,
|
||||
io_unavailable,
|
||||
access_denied,
|
||||
not_found,
|
||||
};
|
||||
|
||||
native_file() = default;
|
||||
|
||||
close_result close() {
|
||||
if (SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable;
|
||||
|
||||
this->rw_ops = nullptr;
|
||||
|
||||
return close_result::ok;
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
return this->rw_ops != nullptr;
|
||||
}
|
||||
|
||||
open_result open(native_path const & file_path, open_mode mode) {
|
||||
if (this->is_open()) switch (this->close()) {
|
||||
case close_result::ok: break;
|
||||
case close_result::io_unavailable: return open_result::io_unavailable;
|
||||
default: coral::unreachable();
|
||||
}
|
||||
|
||||
// No room for zero terminator.
|
||||
if (file_path.is_full()) return open_result::not_found;
|
||||
|
||||
switch (mode) {
|
||||
case open_mode::read_only: {
|
||||
this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "rb");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case open_mode::overwrite: {
|
||||
this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "wb");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: coral::unreachable();
|
||||
}
|
||||
|
||||
if (this->rw_ops == nullptr) return open_result::not_found;
|
||||
|
||||
return open_result::ok;
|
||||
}
|
||||
|
||||
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & data) override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
coral::usize const data_read{SDL_RWread(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)};
|
||||
|
||||
if ((data_read == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable;
|
||||
|
||||
return data_read;
|
||||
}
|
||||
|
||||
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
// TODO: Fix cast.
|
||||
coral::i64 const byte_position{
|
||||
SDL_RWseek(this->rw_ops, static_cast<coral::i64>(offset), RW_SEEK_SET)};
|
||||
|
||||
if (byte_position == -1) return coral::io_error::unavailable;
|
||||
|
||||
return static_cast<coral::u64>(byte_position);
|
||||
}
|
||||
|
||||
coral::expected<coral::u64, coral::io_error> tell() override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
coral::i64 const byte_position{SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)};
|
||||
|
||||
if (byte_position == -1) return coral::io_error::unavailable;
|
||||
|
||||
return static_cast<coral::u64>(byte_position);
|
||||
}
|
||||
|
||||
coral::expected<coral::usize, coral::io_error> write(coral::slice<coral::u8 const> const & data) override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
coral::usize const data_written{SDL_RWwrite(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)};
|
||||
|
||||
if ((data_written == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable;
|
||||
|
||||
return data_written;
|
||||
}
|
||||
|
||||
private:
|
||||
SDL_RWops * rw_ops{nullptr};
|
||||
};
|
||||
|
||||
struct sandboxed_fs : public coral::fs {
|
||||
sandboxed_fs() {
|
||||
char * const path{SDL_GetBasePath()};
|
||||
|
||||
if (path == nullptr) return;
|
||||
|
||||
for (coral::usize index = 0; path[index] != 0; index += 1)
|
||||
this->sandbox_path.put(path[index]);
|
||||
|
||||
SDL_free(path);
|
||||
|
||||
this->access_rules.can_read = true;
|
||||
}
|
||||
|
||||
sandboxed_fs(coral::path const & organization_name, coral::path const & app_name) {
|
||||
char * const path{SDL_GetPrefPath(organization_name.begin(), app_name.begin())};
|
||||
|
||||
if (path == nullptr) return;
|
||||
|
||||
for (coral::usize index = 0; path[index] != 0; index += 1)
|
||||
this->sandbox_path.put(path[index]);
|
||||
|
||||
SDL_free(path);
|
||||
|
||||
this->access_rules.can_read = true;
|
||||
}
|
||||
|
||||
access_rules query_access() override {
|
||||
return this->access_rules;
|
||||
}
|
||||
|
||||
void read_file(coral::path const & file_path, coral::closure<void(coral::file_reader &)> const & then) override {
|
||||
if (!this->access_rules.can_read) return;
|
||||
|
||||
native_path sandbox_file_path;
|
||||
{
|
||||
coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice());
|
||||
|
||||
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
|
||||
}
|
||||
|
||||
{
|
||||
coral::expected const written =
|
||||
sandbox_file_path.write(file_path.as_slice().as_bytes());
|
||||
|
||||
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
|
||||
}
|
||||
|
||||
native_file file;
|
||||
|
||||
if (file.open(sandbox_file_path, native_file::open_mode::read_only) !=
|
||||
native_file::open_result::ok) return;
|
||||
|
||||
then(file);
|
||||
|
||||
if (file.close() != native_file::close_result::ok)
|
||||
// Error orphaned file handle!
|
||||
return;
|
||||
}
|
||||
|
||||
void write_file(coral::path const & file_path, coral::closure<void(coral::file_writer &)> const & then) override {
|
||||
if (!this->access_rules.can_write) return;
|
||||
|
||||
native_path sandbox_file_path;
|
||||
{
|
||||
coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice());
|
||||
|
||||
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
|
||||
}
|
||||
|
||||
{
|
||||
coral::expected const written =
|
||||
sandbox_file_path.write(file_path.as_slice().as_bytes());
|
||||
|
||||
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
|
||||
}
|
||||
|
||||
native_file file;
|
||||
|
||||
if (file.open(sandbox_file_path, native_file::open_mode::overwrite) !=
|
||||
native_file::open_result::ok) return;
|
||||
|
||||
then(file);
|
||||
|
||||
if (file.close() != native_file::close_result::ok)
|
||||
// Error orphaned file handle!
|
||||
return;
|
||||
}
|
||||
|
||||
private:
|
||||
native_path sandbox_path;
|
||||
|
||||
access_rules access_rules{
|
||||
.can_read = false,
|
||||
.can_write = false,
|
||||
};
|
||||
};
|
||||
|
||||
export namespace app {
|
||||
enum class log_level {
|
||||
notice,
|
||||
warning,
|
||||
error,
|
||||
};
|
||||
|
||||
struct client {
|
||||
coral::fs & base() {
|
||||
return this->base_sandbox;
|
||||
}
|
||||
|
||||
void display(coral::u16 screen_width, coral::u16 screen_height) {
|
||||
SDL_SetWindowSize(this->window, screen_width, screen_height);
|
||||
SDL_ShowWindow(this->window);
|
||||
}
|
||||
|
||||
void log(log_level level, coral::slice<char const> const & message) {
|
||||
coral::i32 const length{static_cast<coral::i32>(
|
||||
coral::min(message.length, static_cast<size_t>(coral::i32_max)))};
|
||||
|
||||
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
|
||||
SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer);
|
||||
}
|
||||
|
||||
bool poll() {
|
||||
while (SDL_PollEvent(&this->event) != 0) {
|
||||
switch (this->event.type) {
|
||||
case SDL_QUIT: return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
coral::fs & resources() {
|
||||
return this->resources_archive;
|
||||
}
|
||||
|
||||
static int run(coral::path const & title, coral::closure<int(client &)> const & start) {
|
||||
constexpr int windowpos {SDL_WINDOWPOS_UNDEFINED};
|
||||
constexpr coral::u32 windowflags {SDL_WINDOW_HIDDEN};
|
||||
constexpr int window_width {640};
|
||||
constexpr int window_height {480};
|
||||
|
||||
SDL_Window * const window {SDL_CreateWindow(
|
||||
title.begin(), windowpos, windowpos, window_width, window_height, windowflags)};
|
||||
|
||||
if (window == nullptr) return 0xff;
|
||||
|
||||
struct : public coral::allocator {
|
||||
coral::u8 * reallocate(coral::u8 * allocation, coral::usize requested_size) override {
|
||||
return reinterpret_cast<coral::u8 *>(SDL_realloc(allocation, requested_size));
|
||||
}
|
||||
|
||||
void deallocate(void * allocation) override {
|
||||
SDL_free(allocation);
|
||||
}
|
||||
} allocator;
|
||||
|
||||
client app_client {&allocator, window, title};
|
||||
|
||||
return start(app_client);
|
||||
}
|
||||
|
||||
coral::allocator & thread_safe_allocator() {
|
||||
return *this->allocator;
|
||||
}
|
||||
|
||||
coral::fs & user() {
|
||||
return this->user_sandbox;
|
||||
}
|
||||
|
||||
private:
|
||||
client(coral::allocator * allocator, SDL_Window * window,
|
||||
coral::path const & title) : user_sandbox{"ona", title} {
|
||||
|
||||
this->allocator = allocator;
|
||||
this->window = window;
|
||||
}
|
||||
|
||||
coral::allocator * allocator;
|
||||
|
||||
SDL_Window * window;
|
||||
|
||||
SDL_Event event;
|
||||
|
||||
sandboxed_fs base_sandbox;
|
||||
|
||||
sandboxed_fs user_sandbox;
|
||||
|
||||
oar::archive resources_archive{&base_sandbox, "base.oar"};
|
||||
};
|
||||
}
|
604
source/turtle.cpp
Executable file
604
source/turtle.cpp
Executable file
@ -0,0 +1,604 @@
|
||||
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;
|
||||
}
|
||||
}
|
54
source/turtle/io.cpp
Normal file
54
source/turtle/io.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
export module turtle.io;
|
||||
|
||||
import coral;
|
||||
import coral.files;
|
||||
|
||||
import turtle;
|
||||
|
||||
export namespace turtle {
|
||||
enum class log_level {
|
||||
notice,
|
||||
warning,
|
||||
error,
|
||||
};
|
||||
|
||||
struct event_loop {
|
||||
void log(log_level level, coral::slice<char const> const & message) {
|
||||
static_cast<void>(output().write(message.as_chars().as_bytes()));
|
||||
static_cast<void>(output().write(coral::slice{"\n"}.as_bytes()));
|
||||
}
|
||||
|
||||
bool poll() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static int run(coral::path const & title, coral::closure<int(event_loop &)> execute) {
|
||||
event_loop loop{title};
|
||||
|
||||
return execute(loop);
|
||||
}
|
||||
|
||||
private:
|
||||
coral::path title;
|
||||
|
||||
event_loop(coral::path const & title) {
|
||||
this->title = title;
|
||||
}
|
||||
};
|
||||
|
||||
struct system_allocator : public coral::allocator {
|
||||
system_allocator() = default;
|
||||
|
||||
// TODO: implement thread-safety.
|
||||
|
||||
coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override {
|
||||
if (maybe_allocation != nullptr) coral::unreachable();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void deallocate(void * allocation) override {
|
||||
if (allocation != nullptr) coral::unreachable();
|
||||
}
|
||||
};
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user