Compare commits

..

13 Commits

13 changed files with 734 additions and 379 deletions

0
.vscode/c_cpp_properties.json vendored Normal file → Executable file
View File

0
.vscode/launch.json vendored Normal file → Executable file
View File

2
.vscode/settings.json vendored Normal file → Executable file
View File

@ -50,4 +50,6 @@
"editor.insertSpaces": false,
"C_Cpp.errorSquiggles": "disabled",
"editor.rulers": [120],
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
}

0
.vscode/tasks.json vendored Normal file → Executable file
View File

View File

@ -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)

View File

@ -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"};
};
}

View File

@ -50,6 +50,8 @@ export namespace coral {
using i64 = int64_t;
usize const i64_max = 0xffffffffffffffff;
using f32 = float;
using f64 = double;
@ -317,20 +319,22 @@ export namespace coral {
* lifetime of any functor assigned to it.
*/
template<typename result, typename... arguments> struct closure<result(arguments...)> {
template<typename callable> closure(callable call) requires function_pointer<callable, arguments...> {
this->dispatch = [](void * context, arguments... dispatch_arguments) -> result {
return (reinterpret_cast<callable>(context))(dispatch_arguments...);
};
template<typename callable> closure(callable && call)
requires (functor<callable, arguments...> || function_pointer<callable, arguments...>) {
this->context = reinterpret_cast<void *>(call);
}
if constexpr (functor<callable, arguments...>) {
this->dispatch = [](void * context, arguments... dispatch_arguments) -> result {
return (*reinterpret_cast<callable *>(context))(dispatch_arguments...);
};
template<typename callable> closure(callable && call) requires functor<callable, arguments...> {
this->dispatch = [](void * context, arguments... dispatch_arguments) -> result {
return (*reinterpret_cast<callable *>(context))(dispatch_arguments...);
};
this->context = &call;
} else if constexpr (function_pointer<callable, arguments...>) {
this->dispatch = [](void * context, arguments... dispatch_arguments) -> result {
return (reinterpret_cast<callable>(context))(dispatch_arguments...);
};
this->context = &call;
this->context = reinterpret_cast<void *>(call);
}
}
template<typename callable> closure(callable & call) requires functor<callable, arguments...> {
@ -446,20 +450,27 @@ export namespace coral {
template<typename value> using rebound = expected<value, errors>;
/**
* Constructs from `value`, creating an [expected] that contains the expected type.
* Constructs from `value`, creating an [expected] with the expected type.
*/
expected(expects const & value) {
constexpr expected(expects const & value) {
(*reinterpret_cast<expects *>(this->buffer)) = value;
this->buffer[buffer_size] = 1;
}
/**
* Constructs from `error`, creating an [expected] that does not contain the expected type.
* Constructs from `error`, creating an [expected] with an error.
*/
expected(errors const & error) {
constexpr expected(errors const & error) {
(*reinterpret_cast<errors *>(this->buffer)) = error;
}
/**
* Invokes the `apply` procedure if the expected is not ok, otherwise having no side-effects.
*/
void and_then(closure<void(expects &)> const & apply) {
if (this->is_ok()) apply(*this->ok());
}
/**
* Returns the contained error as an [optional].
*/

View File

@ -33,7 +33,7 @@ export namespace coral {
* Returns a weak reference to the [path] as a [slice].
*/
constexpr slice<char const> as_slice() const {
return {this->buffer, this->byte_size()};
return {this->buffer, this->filled()};
}
/**
@ -45,13 +45,6 @@ export namespace coral {
return reinterpret_cast<char const *>(this->buffer);
}
/**
* Returns the number of bytes composing the path.
*/
constexpr usize byte_size() const {
return max - this->buffer[max];
}
/**
* Compares the path to `that`, returning the difference between the two paths or `0` if they are identical.
*/
@ -63,7 +56,7 @@ export namespace coral {
* Returns the tail pointer of the path name.
*/
char const * end() const {
return this->buffer + this->byte_size();
return this->buffer + this->filled();
}
/**
@ -73,6 +66,13 @@ export namespace coral {
return coral::equals(this->as_slice().as_bytes(), that.as_slice().as_bytes());
}
/**
* Returns the number of characters composing the path.
*/
constexpr usize filled() const {
return max - this->buffer[max];
}
/**
* Returns the path hash code.
*
@ -95,7 +95,7 @@ export namespace coral {
path joined_path = *this;
for (char const c : text) {
joined_path.buffer[joined_path.byte_size()] = c;
joined_path.buffer[joined_path.filled()] = c;
joined_path.buffer[max] -= 1;
}
@ -167,7 +167,7 @@ export namespace coral {
/**
* Attempts to read the file in `target_path`, calling `then` if it was successfully opened for reading and
* passing the [file_reader] context along.
*
*
* See [file_reader] for more information on how to read from the file.
*/
virtual void read_file(path const & target_path, closure<void(file_reader &)> const & then) {}
@ -183,9 +183,9 @@ export namespace coral {
/**
* Attempts to write a file in the file system located at `target_path`, calling `then` if it was successfully
* created and / or opened for writing and passing the [file_writer] context along.
*
*
* See [file_writer] for more information on how to write to the file.
*
*
* *Note*: Any file already existing at `target_path` will be overwritten to create a new file for writing.
*/
virtual void write_file(path const & target_path, closure<void(file_writer &)> const & then) {}

View File

@ -17,7 +17,7 @@ export namespace coral {
* [fixed_buffer] is not mutated or out-of-scope.
*/
slice<u8 const> as_slice() const {
return {0, this->filled};
return {this->data, this->data_filled};
}
/**
@ -27,13 +27,6 @@ export namespace coral {
return this->data;
}
/**
* Returns the number of bytes in the buffer that have been filled with data.
*/
usize count() const {
return this->filled;
}
/**
* Returns the tail pointer of the buffer data.
*/
@ -41,18 +34,25 @@ export namespace coral {
return this->data + this->cursor;
}
/**
* Returns the number of bytes in the buffer that have been filled with data.
*/
usize filled() const {
return this->data_filled;
}
/**
* Returns `true` if the buffer is completely empty of data, otherwise `false`.
*/
bool is_empty() const {
return this->filled == capacity;
return this->data_filled == capacity;
}
/**
* Returns `true` if the buffer has been completely filled with data, otherwise `false`.
*/
bool is_full() const {
return this->filled == capacity;
return this->data_filled == capacity;
}
/**
@ -62,7 +62,7 @@ export namespace coral {
bool put(u8 data) {
if (this->is_full()) return false;
this->filled += 1;
this->data_filled += 1;
this->data[this->write_index] = data;
this->write_index = (this->write_index + 1) % capacity;
@ -73,9 +73,9 @@ export namespace coral {
* Reads whatever data is in the buffer into `data`, returning the number of bytes read from the buffer.
*/
expected<usize, io_error> read(slice<u8> const & data) override {
slice const readable_data{this->data, min(this->filled, data.length)};
slice const readable_data {this->data, min(this->data_filled, data.length)};
this->filled -= readable_data.length;
this->data_filled -= readable_data.length;
for (usize index = 0; index < readable_data.length; index += 1) {
data[index] = this->data[this->read_index];
@ -85,6 +85,13 @@ export namespace coral {
return readable_data.length;
}
/**
* Returns the remaining unfilled buffer space in bytes.
*/
usize remaining() const {
return capacity - this->data_filled;
}
/**
* Attempts to write `data` to the buffer, returning the number of bytes written or [io_error::unavailable] if
* it has been completely filled and no more bytes can be written.
@ -92,9 +99,9 @@ export namespace coral {
expected<usize, io_error> write(slice<u8 const> const & data) override {
if (this->is_full()) return io_error::unavailable;
slice const writable_data{data.sliced(0, min(data.length, this->filled))};
slice const writable_data {data.sliced(0, min(data.length, this->remaining()))};
this->filled += writable_data.length;
this->data_filled += writable_data.length;
for (usize index = 0; index < writable_data.length; index += 1) {
this->data[this->write_index] = data[index];
@ -105,7 +112,7 @@ export namespace coral {
}
private:
usize filled {0};
usize data_filled {0};
usize read_index {0};

View File

@ -73,7 +73,7 @@ export namespace coral {
*
* *Note*: the [allocator] referenced in the stack must remain valid for the duration of the stack lifetime.
*/
template<typename element, usize init_capacity = 1> struct small_stack : public stack<element> {
template<typename element, usize init_capacity = 1> struct small_stack final : public stack<element> {
small_stack(allocator & dynamic_allocator) : dynamic_allocator{dynamic_allocator} {}
~small_stack() override {
@ -255,9 +255,8 @@ export namespace coral {
* Readable type for streaming data from a [stack] containing [u8] values.
*/
struct stack_reader : public reader {
stack_reader(byte_stack const * stack) {
stack_reader(byte_stack const & stack) : stack{stack} {
this->cursor = 0;
this->stack = stack;
}
/**
@ -266,7 +265,7 @@ export namespace coral {
expected<usize, io_error> read(slice<u8> const & buffer) override {
usize data_written = 0;
this->stack->every([&](u8 byte) -> bool {
this->stack.every([&](u8 byte) -> bool {
buffer[data_written] = byte;
data_written += 1;
@ -281,23 +280,21 @@ export namespace coral {
private:
usize cursor {0};
byte_stack const * stack {nullptr};
byte_stack const & stack;
};
/**
* Writable type for appending data to a [contiguous_range] containing [u8] values.
*/
struct stack_writer : public writer {
stack_writer(byte_stack * stack) {
this->stack = stack;
}
stack_writer(byte_stack & stack) : stack{stack} {}
/**
* Attempts to write `buffer` to the target stack, returning the number of bytes written or an [io_error] if it
* failed to commit `buffer` to the stack memory.
*/
expected<usize, io_error> write(slice<u8 const> const & buffer) override {
switch (this->stack->push_all(buffer)) {
switch (this->stack.push_all(buffer)) {
case push_result::ok: return buffer.length;
case push_result::out_of_memory: return io_error::unavailable;
default: unreachable();
@ -305,6 +302,6 @@ export namespace coral {
}
private:
byte_stack * stack {nullptr};
byte_stack & stack;
};
}

View File

@ -260,8 +260,7 @@ struct walker final : public file_walker {
export namespace oar {
struct archive : public fs {
archive(fs * backing_fs, path const & archive_path) {
this->backing_fs = backing_fs;
archive(fs & backing_fs, path const & archive_path) : backing_fs{backing_fs} {
this->archive_path = archive_path;
}
@ -269,7 +268,7 @@ export namespace oar {
* See [fs::walk_files].
*/
void walk_files(path const & target_path, closure<void(file_walker &)> const & then) override {
this->backing_fs->read_file(this->archive_path, [&](file_reader & archive_reader) {
this->backing_fs.read_file(this->archive_path, [&](file_reader & archive_reader) {
entry archive_entry{&archive_reader};
if (archive_entry.find(entry_kind::directory, target_path) != entry::find_result::ok) return;
@ -284,9 +283,7 @@ export namespace oar {
* See [fs::read_file].
*/
void read_file(path const & file_path, closure<void(file_reader &)> const & then) override {
if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return;
this->backing_fs->read_file(this->archive_path, [&](file_reader & archive_reader) {
this->backing_fs.read_file(this->archive_path, [&](file_reader & archive_reader) {
entry archive_entry {&archive_reader};
if (archive_entry.find(entry_kind::file, file_path) != entry::find_result::ok) return;
@ -296,7 +293,7 @@ export namespace oar {
}
private:
fs * backing_fs;
fs & backing_fs;
path archive_path;
};

604
source/turtle.cpp Executable file
View 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
View 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();
}
};
};