Compare commits
13 Commits
bb9617994f
...
261b62fc2d
Author | SHA1 | Date | |
---|---|---|---|
261b62fc2d | |||
3e413ea45d | |||
c16d5c08cb | |||
1954f97666 | |||
f6daa3c85a | |||
d8aadef0e7 | |||
28eeacfaa9 | |||
8023e09712 | |||
99fab07efa | |||
d6f08efd55 | |||
d6f6bc246e | |||
c8563b4e9d | |||
ea7435acc6 |
0
.vscode/c_cpp_properties.json
vendored
Normal file → Executable file
0
.vscode/c_cpp_properties.json
vendored
Normal file → Executable file
0
.vscode/launch.json
vendored
Normal file → Executable file
0
.vscode/launch.json
vendored
Normal file → Executable file
2
.vscode/settings.json
vendored
Normal file → Executable file
2
.vscode/settings.json
vendored
Normal file → Executable 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
0
.vscode/tasks.json
vendored
Normal file → Executable file
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"};
|
||||
};
|
||||
}
|
@ -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...> {
|
||||
template<typename callable> closure(callable && call)
|
||||
requires (functor<callable, arguments...> || function_pointer<callable, arguments...>) {
|
||||
|
||||
if constexpr (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 = reinterpret_cast<void *>(call);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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].
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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
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