Compare commits
17 Commits
261b62fc2d
...
7b0f526b55
Author | SHA1 | Date | |
---|---|---|---|
7b0f526b55 | |||
fc7a5fa89f | |||
e9c877956e | |||
398e28d328 | |||
7878c52496 | |||
1b6e80f3f1 | |||
3b9e00b1cb | |||
530535b2a9 | |||
a44aed2fe3 | |||
36ca698e22 | |||
dee82f6437 | |||
e5e4f11004 | |||
356aa6729b | |||
57d94ef8b9 | |||
7876efc4f5 | |||
5755cc6517 | |||
c53fd30cc2 |
@ -223,72 +223,6 @@ export namespace coral {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates and initializes a type of `requested_size` in `buffer`, returning its base pointer. As a result of
|
||||
* accepting a pre-allocated buffer, invocation does not allocate any dynamic memory.
|
||||
*
|
||||
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked behavior.
|
||||
*/
|
||||
export void * operator new(coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
|
||||
if (buffer.length < requested_size) coral::unreachable();
|
||||
|
||||
return buffer.pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates and initializes a series of types at `requested_size` in `buffer`, returning the base pointer. As a result
|
||||
* of accepting a pre-allocated buffer, invocation does not allocate any dynamic memory.
|
||||
*
|
||||
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked behavior.
|
||||
*/
|
||||
export void * operator new[](coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
|
||||
if (buffer.length < requested_size) coral::unreachable();
|
||||
|
||||
return buffer.pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to allocate and initialize a type of `requested_size` using `allocator`.
|
||||
*
|
||||
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program exit. This may be
|
||||
* achieved through either [coral::allocator::deallocate] or implementation-specific allocator functionality.
|
||||
*/
|
||||
export [[nodiscard]] void * operator new(coral::usize requested_size, coral::allocator & allocator) {
|
||||
return allocator.reallocate(nullptr, requested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to allocate and initialize a series of types of `requested_size` using `allocator`.
|
||||
*
|
||||
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program exit. This may be
|
||||
* achieved through either [coral::allocator::deallocate] or implementation-specific allocator functionality.
|
||||
*/
|
||||
export [[nodiscard]] void * operator new[](coral::usize requested_size, coral::allocator & allocator) {
|
||||
return allocator.reallocate(nullptr, requested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* If `pointer` is a non-`nullptr` value, the referenced memory will be deallocated using `allocator`. Otherwise, the
|
||||
* function has no side-effects.
|
||||
*
|
||||
* *Note*: passing a `pointer` value that was not allocated by `allocator` will result in erroneous behavior defined by
|
||||
* the [coral::allocator] implementation.
|
||||
*/
|
||||
export void operator delete(void * pointer, coral::allocator & allocator) {
|
||||
return allocator.deallocate(pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* If `pointer` is a non-`nullptr` value, the referenced memory block will be deallocated using `allocator`. Otherwise,
|
||||
* the function has no side-effects.
|
||||
*
|
||||
* *Note*: passing a `pointer` value that was not allocated by `allocator` will result in erroneous behavior defined by
|
||||
* the [coral::allocator] implementation.
|
||||
*/
|
||||
export void operator delete[](void * pointer, coral::allocator & allocator) {
|
||||
return allocator.deallocate(pointer);
|
||||
}
|
||||
|
||||
// Wrapper types.
|
||||
export namespace coral {
|
||||
template<typename callable, typename... arguments> concept function_pointer =
|
||||
@ -561,6 +495,35 @@ export namespace coral {
|
||||
*/
|
||||
virtual expected<usize, io_error> write(slice<u8 const> const & data) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sequence enumeration interface.
|
||||
*/
|
||||
template<typename type> struct enumerator {
|
||||
virtual ~enumerator() {}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve the next file path in the file tree, returning true if another value was retrieved or
|
||||
* `false` if there are no more values to be enumerated over.
|
||||
*
|
||||
* ```cpp
|
||||
* while (sequence.enumerate()) {
|
||||
* do_thing(sequence.value());
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The enumerated value may be accessed through [value].
|
||||
*/
|
||||
virtual bool enumerate() = 0;
|
||||
|
||||
/**
|
||||
* Returns the last-enumerated value.
|
||||
*
|
||||
* *Note*: calling this function before first having recieved a `true` value from [enumerate] will result in
|
||||
* implementation-defined behaviour.
|
||||
*/
|
||||
virtual type value() = 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Input/output operations.
|
||||
|
@ -3,109 +3,6 @@ export module coral.files;
|
||||
import coral;
|
||||
|
||||
export namespace coral {
|
||||
/**
|
||||
* Platform-generalized identifier for a resource in a [fs].
|
||||
*/
|
||||
struct path {
|
||||
/**
|
||||
* Maximum path length.
|
||||
*/
|
||||
static usize const max = u8_max;
|
||||
|
||||
/**
|
||||
* Common path component separator.
|
||||
*/
|
||||
static char const seperator = '/';
|
||||
|
||||
constexpr path() {
|
||||
this->buffer[max] = max;
|
||||
}
|
||||
|
||||
template<usize text_size> constexpr 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[max] = max - text_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a weak reference to the [path] as a [slice].
|
||||
*/
|
||||
constexpr slice<char const> as_slice() const {
|
||||
return {this->buffer, this->filled()};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base pointer of the path name.
|
||||
*
|
||||
* *Note*: the returned buffer pointer is guaranteed to end with a zero terminator.
|
||||
*/
|
||||
char const * begin() const {
|
||||
return reinterpret_cast<char const *>(this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the path to `that`, returning the difference between the two paths or `0` if they are identical.
|
||||
*/
|
||||
constexpr size compare(path const & that) const {
|
||||
return coral::compare(this->as_slice().as_bytes(), that.as_slice().as_bytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tail pointer of the path name.
|
||||
*/
|
||||
char const * end() const {
|
||||
return this->buffer + this->filled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the path against `that` for equality, returning `true` if they are identical, otherwise `false`.
|
||||
*/
|
||||
constexpr bool equals(path const & that) const {
|
||||
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.
|
||||
*
|
||||
* *Note:* the returned hash code is not guaranteed to be unique.
|
||||
*/
|
||||
constexpr u64 hash() const {
|
||||
return coral::hash(this->as_slice().as_bytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new [path] composed of the current path joined with `text`.
|
||||
*
|
||||
* *Note:* should the new path exceed [max] bytes in size, an empty [path] is returned instead.
|
||||
*
|
||||
* *Note:* should the new path exceed [max] bytes in size, an empty [path] is returned instead.
|
||||
*/
|
||||
constexpr path joined(slice<char const> const & text) const {
|
||||
if (text.length > this->buffer[max]) return path{};
|
||||
|
||||
path joined_path = *this;
|
||||
|
||||
for (char const c : text) {
|
||||
joined_path.buffer[joined_path.filled()] = c;
|
||||
joined_path.buffer[max] -= 1;
|
||||
}
|
||||
|
||||
return joined_path;
|
||||
}
|
||||
|
||||
private:
|
||||
char buffer[max + 1]{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* [reader] that has a known range of data and may attempt to traverse it freely.
|
||||
*/
|
||||
@ -124,21 +21,23 @@ export namespace coral {
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerator a file tree recursively, returning the absolute path of each file within.
|
||||
* Enumerates a file tree recursively, returning the absolute path of each file within.
|
||||
*
|
||||
* *Note*: file tree walking order is considered implementation-specific.
|
||||
*/
|
||||
struct file_walker {
|
||||
struct file_walker : public enumerator<expected<slice<char const>, io_error>> {
|
||||
virtual ~file_walker() {}
|
||||
|
||||
/**
|
||||
* Returns `true` if there are paths pending enumeration, otherwise `false`.
|
||||
* See [enumerator::enumerate].
|
||||
*/
|
||||
virtual bool has_next() = 0;
|
||||
virtual bool enumerate() = 0;
|
||||
|
||||
/**
|
||||
* Attempts to enumerate over the next absolute [path] in the file tree, returning it or an [io_error] if the
|
||||
* operation failed for whatever reason.
|
||||
* Returns the last-enumerated path as a sequence of `char`s or an [io_error] if the last enumeration did not
|
||||
* successfully complete.
|
||||
*/
|
||||
virtual expected<path, io_error> next() = 0;
|
||||
virtual expected<slice<char const>, io_error> value() = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -161,33 +60,62 @@ export namespace coral {
|
||||
/**
|
||||
* Platform-generalized file system interface.
|
||||
*/
|
||||
struct fs {
|
||||
virtual ~fs() {};
|
||||
struct file_system {
|
||||
/**
|
||||
* Errors that may occur during a file system query operation.
|
||||
*
|
||||
* [query_error::unsupported] occurs when either part or all of the query operation is not supported on the
|
||||
* given file system.
|
||||
*
|
||||
* [query_error::io_unavailable] is a general catch-all to report that an implementation-specific I/O error has
|
||||
* occured and the operation could not proceed as a result.
|
||||
*/
|
||||
enum class query_error {
|
||||
unsupported,
|
||||
io_unavailable,
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to read the file in `target_path`, calling `then` if it was successfully opened for reading and
|
||||
* passing the [file_reader] context along.
|
||||
* Various meta-information about a file in a file system.
|
||||
*/
|
||||
struct file_info {
|
||||
u64 size;
|
||||
};
|
||||
|
||||
virtual ~file_system() {};
|
||||
|
||||
/**
|
||||
* Attempts to query a file in the file system located at `path`, returning a [file_info] describing it or a
|
||||
* [query_error] if the operation failed.
|
||||
*/
|
||||
virtual expected<file_info, query_error> query_file(slice<char const> const & path) {
|
||||
return query_error::unsupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read a file in the file system located at `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) {}
|
||||
virtual void read_file(slice<char const> const & path, closure<void(file_reader &)> const & then) {}
|
||||
|
||||
/**
|
||||
* Attempts to walk the file tree from `target_path`, calling `then` if it was successfully opened for walking
|
||||
* and passing the [file_walker] context along.
|
||||
* Attempts to walk the file tree from `path`, calling `then` if it was successfully opened for walking and
|
||||
* passing the [file_walker] context along.
|
||||
*
|
||||
* See [file_walker] for more information on how to traverse the file tree.
|
||||
*/
|
||||
virtual void walk_files(path const & target_path, closure<void(file_walker &)> const & then) {}
|
||||
virtual void walk_files(slice<char const> const & path, closure<void(file_walker &)> const & then) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Attempts to write a file in the file system located at `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.
|
||||
* *Note*: Any file already existing at `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) {}
|
||||
virtual void write_file(slice<char const> const & path, closure<void(file_writer &)> const & then) {}
|
||||
};
|
||||
}
|
||||
|
@ -4,34 +4,76 @@ import coral;
|
||||
|
||||
export namespace coral {
|
||||
/**
|
||||
* Multiplexing byte-based ring buffer of `capacity` size that may be used for memory-backed I/O operations and
|
||||
* lightweight data construction.
|
||||
* Multiplexing byte-based ring buffer that may be used for memory-backed I/O operations and fast data (re)-
|
||||
* construction.
|
||||
*/
|
||||
template<usize capacity> struct fixed_buffer : public writer, public reader {
|
||||
fixed_buffer() = default;
|
||||
struct ring_buffer : public writer, public reader {
|
||||
/**
|
||||
* Potential results from attempting to allocate a ring buffer dynamically.
|
||||
*
|
||||
* [allocate_result::ok] means nothing went wrong and the allocation was successful.
|
||||
*
|
||||
* [allocate_result::out_of_memory] indicates that there is not enough memory available in the provided
|
||||
* allocator to create a buffer of the requested size.
|
||||
*/
|
||||
enum class [[nodiscard]] allocate_result {
|
||||
ok,
|
||||
out_of_memory,
|
||||
};
|
||||
|
||||
ring_buffer() = default;
|
||||
|
||||
ring_buffer(ring_buffer const &) = delete;
|
||||
|
||||
~ring_buffer() override {
|
||||
if (this->data_allocator != nullptr) this->data_allocator->deallocate(this->data.pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to allocate memory for the ring buffer `capacity` number of bytes large with `data_allocator` as the
|
||||
* allocation strategy.
|
||||
*
|
||||
* See [allocate_result] for more information on error handling.
|
||||
*
|
||||
* *Note*: calling allocate will wipe out the contents of the current buffer.
|
||||
*/
|
||||
allocate_result allocate(allocator & data_allocator, usize capacity) {
|
||||
if (this->data_allocator != nullptr) this->data_allocator->deallocate(this->data.pointer);
|
||||
|
||||
this->clear();
|
||||
|
||||
u8 * const data {this->data_allocator->reallocate(nullptr, capacity)};
|
||||
|
||||
if (data == nullptr) {
|
||||
this->data_allocator = nullptr;
|
||||
this->data = {};
|
||||
|
||||
return allocate_result::out_of_memory;
|
||||
}
|
||||
|
||||
this->data_allocator = &data_allocator;
|
||||
this->data = {data, capacity};
|
||||
|
||||
return allocate_result::ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable [slice] ranging from the head to the last-filled element.
|
||||
*
|
||||
* *Note*: The lifetime and validity of the returned slice is only guaranteed for as long as the source
|
||||
* [fixed_buffer] is not mutated or out-of-scope.
|
||||
* [ring_buffer] is not mutated or out-of-scope.
|
||||
*/
|
||||
slice<u8 const> as_slice() const {
|
||||
return {this->data, this->data_filled};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base pointer of the buffer data.
|
||||
*/
|
||||
u8 const * begin() const {
|
||||
return this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tail pointer of the buffer data.
|
||||
* Clears all written elements from the buffer.
|
||||
*/
|
||||
u8 const * end() const {
|
||||
return this->data + this->cursor;
|
||||
void clear() {
|
||||
this->data_filled = 0;
|
||||
this->read_index = 0;
|
||||
this->write_index = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,14 +87,14 @@ export namespace coral {
|
||||
* Returns `true` if the buffer is completely empty of data, otherwise `false`.
|
||||
*/
|
||||
bool is_empty() const {
|
||||
return this->data_filled == capacity;
|
||||
return this->data_filled == this->data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the buffer has been completely filled with data, otherwise `false`.
|
||||
*/
|
||||
bool is_full() const {
|
||||
return this->data_filled == capacity;
|
||||
return this->data_filled == this->data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,7 +106,7 @@ export namespace coral {
|
||||
|
||||
this->data_filled += 1;
|
||||
this->data[this->write_index] = data;
|
||||
this->write_index = (this->write_index + 1) % capacity;
|
||||
this->write_index = (this->write_index + 1) % this->data.length;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -73,13 +115,13 @@ 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->data_filled, data.length)};
|
||||
slice const readable_data {this->data.sliced(0, min(this->data_filled, 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];
|
||||
this->read_index = (this->read_index + 1) % capacity;
|
||||
this->read_index = (this->read_index + 1) % this->data.length;
|
||||
}
|
||||
|
||||
return readable_data.length;
|
||||
@ -89,23 +131,20 @@ export namespace coral {
|
||||
* Returns the remaining unfilled buffer space in bytes.
|
||||
*/
|
||||
usize remaining() const {
|
||||
return capacity - this->data_filled;
|
||||
return this->data.length - 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.
|
||||
* Write `data` to the buffer, returning the number of bytes written to the buffer.
|
||||
*/
|
||||
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->remaining()))};
|
||||
|
||||
this->data_filled += writable_data.length;
|
||||
|
||||
for (usize index = 0; index < writable_data.length; index += 1) {
|
||||
this->data[this->write_index] = data[index];
|
||||
this->write_index = (this->write_index + 1) % capacity;
|
||||
this->write_index = (this->write_index + 1) % this->data.length;
|
||||
}
|
||||
|
||||
return writable_data.length;
|
||||
@ -118,7 +157,9 @@ export namespace coral {
|
||||
|
||||
usize write_index {0};
|
||||
|
||||
u8 data[capacity]{0};
|
||||
allocator * data_allocator {nullptr};
|
||||
|
||||
slice<u8> data {};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -201,4 +242,9 @@ export namespace coral {
|
||||
|
||||
return output.write({buffer, buffer_count});
|
||||
}
|
||||
|
||||
template<typename... arguments> expected<usize, io_error> println(writer & output, arguments... print_arguments) {
|
||||
// TODO: Implement.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
444
source/oar.cpp
444
source/oar.cpp
@ -2,408 +2,110 @@ export module oar;
|
||||
|
||||
import coral;
|
||||
import coral.files;
|
||||
import coral.functional;
|
||||
import coral.io;
|
||||
import coral.stack;
|
||||
|
||||
using coral::closure;
|
||||
using coral::expected;
|
||||
using coral::file_reader;
|
||||
using coral::file_walker;
|
||||
using coral::fs;
|
||||
using coral::io_error;
|
||||
using coral::path;
|
||||
using coral::slice;
|
||||
using coral::u8;
|
||||
using coral::u64;
|
||||
using coral::usize;
|
||||
|
||||
/**
|
||||
* Length of the full magic signature at the beginning of an Oar file.
|
||||
*/
|
||||
constexpr static usize signature_length {4};
|
||||
|
||||
/**
|
||||
* Hardcoded signature magic value that this implementation of Oar expects when reading archives.
|
||||
*/
|
||||
constexpr static u8 signature_magic[signature_length] {'o', 'a', 'r', 1};
|
||||
|
||||
/**
|
||||
* Oar file header format.
|
||||
*/
|
||||
union header {
|
||||
struct {
|
||||
u8 signature[signature_length];
|
||||
|
||||
coral::u32 entry_count;
|
||||
} layout;
|
||||
|
||||
u8 bytes[512];
|
||||
|
||||
static constexpr bool is_sizeof(usize value) {
|
||||
return value == sizeof(header);
|
||||
}
|
||||
|
||||
expected<bool, io_error> read(coral::reader & archive_reader) {
|
||||
return archive_reader.read(this->bytes).map<bool>(is_sizeof).map<bool>([&](bool is_valid) -> bool {
|
||||
return is_valid && coral::equals(this->layout.signature, signature_magic);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(header::is_sizeof(512));
|
||||
|
||||
enum class entry_kind {
|
||||
file,
|
||||
directory,
|
||||
};
|
||||
|
||||
/**
|
||||
* Oar entry block format.
|
||||
*/
|
||||
union block {
|
||||
struct {
|
||||
path path;
|
||||
|
||||
u64 data_offset;
|
||||
|
||||
u64 data_length;
|
||||
|
||||
entry_kind kind;
|
||||
} layout;
|
||||
|
||||
u8 bytes[512];
|
||||
|
||||
static constexpr bool is_sizeof(usize value) {
|
||||
return value == sizeof(block);
|
||||
}
|
||||
|
||||
expected<bool, io_error> read(coral::reader & archive_reader) {
|
||||
return archive_reader.read(this->bytes).map<bool>(is_sizeof);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(block::is_sizeof(512));
|
||||
|
||||
/**
|
||||
* Archive entry access interface.
|
||||
*/
|
||||
struct entry final : public file_reader {
|
||||
/**
|
||||
* Results of a find operation performed on an [archive_file].
|
||||
*
|
||||
* [find_result::ok] means that the find operation was successful.
|
||||
*
|
||||
* [find_result::io_unavailable] signals a failure to communicate with the underlying [file_reader] for whatever
|
||||
* reason.
|
||||
*
|
||||
* [find_result::archive_invalid] signals that data was read but it does not match the format of an Oar archive.
|
||||
* This is typically because the underlying [file_reader] is not reading from an Oar archive file or the archive does
|
||||
* not match the supported version.
|
||||
*
|
||||
* [find_result::not_found] indicates that no entry in the archive could be found that matches the given query.
|
||||
*/
|
||||
enum class [[nodiscard]] find_result {
|
||||
ok,
|
||||
io_unavailable,
|
||||
archive_invalid,
|
||||
not_found,
|
||||
};
|
||||
|
||||
entry(file_reader * archive_reader) {
|
||||
this->archive_reader = archive_reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a lookup for a file entry matching the path `file_path` in the archive, returning [find_result] to
|
||||
* indicate the result of the operation.
|
||||
*/
|
||||
find_result find(entry_kind kind, path const & entry_path) {
|
||||
this->data_offset = 0;
|
||||
this->data_length = 0;
|
||||
this->data_cursor = 0;
|
||||
|
||||
if (!this->archive_reader->seek(0).is_ok()) return find_result::io_unavailable;
|
||||
|
||||
header archive_header {};
|
||||
|
||||
if (!archive_header.read(*this->archive_reader).map<bool>(coral::equality_predicate(true)).is_ok())
|
||||
return find_result::archive_invalid;
|
||||
|
||||
// Read file table.
|
||||
u64 head {0};
|
||||
u64 tail {archive_header.layout.entry_count - 1};
|
||||
block archive_block {};
|
||||
|
||||
while (head <= tail) {
|
||||
u64 const midpoint {head + ((tail - head) / 2)};
|
||||
|
||||
if (!archive_block.read(*this->archive_reader).map<bool>(coral::equality_predicate(true)).is_ok())
|
||||
return find_result::archive_invalid;
|
||||
|
||||
if (archive_block.layout.kind == kind) return find_result::not_found;
|
||||
|
||||
coral::size const comparison {entry_path.compare(archive_block.layout.path)};
|
||||
|
||||
if (comparison == 0) {
|
||||
this->data_offset = archive_block.layout.data_offset;
|
||||
this->data_length = archive_block.layout.data_length;
|
||||
this->data_cursor = archive_block.layout.data_offset;
|
||||
|
||||
return find_result::ok;
|
||||
}
|
||||
|
||||
if (comparison > 0) {
|
||||
head = (midpoint + 1);
|
||||
} else {
|
||||
tail = (midpoint - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return find_result::not_found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read `data.length` bytes from the file and fill `data` with it, returning the
|
||||
* number of bytes actually read or a [io_error] value to indicate an error occured.
|
||||
*/
|
||||
expected<usize, io_error> read(coral::slice<u8> const & data) override {
|
||||
if (this->data_offset < sizeof(header)) return io_error::unavailable;
|
||||
|
||||
usize const data_tail {this->data_offset + this->data_length};
|
||||
|
||||
if (!this->archive_reader->seek(coral::clamp(this->data_offset + this->data_cursor,
|
||||
this->data_offset, data_tail)).is_ok()) return io_error::unavailable;
|
||||
|
||||
expected const data_read {this->archive_reader->read(
|
||||
data.sliced(0, coral::min(data.length, data_tail - this->data_cursor)))};
|
||||
|
||||
if (data_read.is_ok()) this->data_cursor += *data_read.ok();
|
||||
|
||||
return data_read;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to seek to `offset` absolute position in the file, returning the new absolute
|
||||
* cursor or a [io_error] value to indicate an error occured.
|
||||
*/
|
||||
expected<u64, io_error> seek(u64 offset) override {
|
||||
if (this->data_offset < sizeof(header)) return io_error::unavailable;
|
||||
|
||||
this->data_cursor = offset;
|
||||
|
||||
return io_error::unavailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read to read the absolute file cursor position, returning it or a [io_error]
|
||||
* value to indicate an error occured.
|
||||
*/
|
||||
expected<u64, io_error> tell() override {
|
||||
if (this->data_offset < sizeof(header)) return io_error::unavailable;
|
||||
|
||||
return this->data_cursor;
|
||||
}
|
||||
|
||||
private:
|
||||
file_reader * archive_reader {nullptr};
|
||||
|
||||
u64 data_offset {0};
|
||||
|
||||
u64 data_length {0};
|
||||
|
||||
u64 data_cursor {0};
|
||||
};
|
||||
|
||||
struct walker final : public file_walker {
|
||||
entry & archive_entry;
|
||||
|
||||
u64 index {0};
|
||||
|
||||
u64 count {0};
|
||||
|
||||
walker(entry & archive_entry) : archive_entry{archive_entry} {}
|
||||
|
||||
bool has_next() override {
|
||||
return this->index < this->count;
|
||||
}
|
||||
|
||||
expected<path, io_error> next() override {
|
||||
constexpr usize path_size {sizeof(path)};
|
||||
u8 path_buffer[path_size] {0};
|
||||
|
||||
// Read verify integrity.
|
||||
{
|
||||
expected const data_read {archive_entry.read(path_buffer)};
|
||||
|
||||
if (data_read.is_error()) return this->error();
|
||||
|
||||
switch (*data_read.ok()) {
|
||||
case path_size: break;
|
||||
case 0: return path{};
|
||||
default: return this->error();
|
||||
}
|
||||
}
|
||||
|
||||
// Verify existence of zero terminator in path.
|
||||
if (!coral::find_last(path_buffer, 0).has_value()) return this->error();
|
||||
|
||||
return path{}.joined(coral::slice{path_buffer}.as_chars());
|
||||
}
|
||||
|
||||
private:
|
||||
io_error error() {
|
||||
this->index = this->count;
|
||||
|
||||
return io_error::unavailable;
|
||||
}
|
||||
};
|
||||
|
||||
export namespace oar {
|
||||
struct archive : public fs {
|
||||
archive(fs & backing_fs, path const & archive_path) : backing_fs{backing_fs} {
|
||||
this->archive_path = archive_path;
|
||||
struct entry_path {
|
||||
enum class parse_error {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximum path length.
|
||||
*/
|
||||
static usize const max = coral::u8_max;
|
||||
|
||||
/**
|
||||
* Common path component separator.
|
||||
*/
|
||||
static char const seperator = '/';
|
||||
|
||||
constexpr entry_path() {
|
||||
this->buffer[max] = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* See [fs::walk_files].
|
||||
* Returns a weak reference to the path as a [slice].
|
||||
*/
|
||||
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) {
|
||||
entry archive_entry{&archive_reader};
|
||||
|
||||
if (archive_entry.find(entry_kind::directory, target_path) != entry::find_result::ok) return;
|
||||
|
||||
walker archive_walker {archive_entry};
|
||||
|
||||
then(archive_walker);
|
||||
});
|
||||
constexpr slice<char const> as_slice() const {
|
||||
return {this->buffer, this->filled()};
|
||||
}
|
||||
|
||||
/**
|
||||
* See [fs::read_file].
|
||||
* Compares the path to `that`, returning the difference between the two paths or `0` if they are identical.
|
||||
*/
|
||||
void read_file(path const & file_path, closure<void(file_reader &)> const & then) override {
|
||||
this->backing_fs.read_file(this->archive_path, [&](file_reader & archive_reader) {
|
||||
entry archive_entry {&archive_reader};
|
||||
constexpr coral::size compare(entry_path const & that) const {
|
||||
return coral::compare(this->as_slice().as_bytes(), that.as_slice().as_bytes());
|
||||
}
|
||||
|
||||
if (archive_entry.find(entry_kind::file, file_path) != entry::find_result::ok) return;
|
||||
/**
|
||||
* Returns the number of characters composing the path.
|
||||
*/
|
||||
constexpr usize filled() const {
|
||||
return max - this->buffer[max];
|
||||
}
|
||||
|
||||
then(archive_entry);
|
||||
});
|
||||
static constexpr expected<entry_path, parse_error> parse(slice<char const> const & text) {
|
||||
// TODO: Implement.
|
||||
return entry_path{};
|
||||
}
|
||||
|
||||
private:
|
||||
fs & backing_fs;
|
||||
|
||||
path archive_path;
|
||||
char buffer[max + 1] {0};
|
||||
};
|
||||
|
||||
enum class [[nodiscard]] bundle_result {
|
||||
ok,
|
||||
out_of_memory,
|
||||
too_many_files,
|
||||
io_error,
|
||||
union entry_block {
|
||||
struct {
|
||||
entry_path path;
|
||||
|
||||
u64 data_offset;
|
||||
|
||||
u64 data_length;
|
||||
} layout;
|
||||
|
||||
u8 bytes[512];
|
||||
|
||||
static constexpr bool is_sizeof(usize value) {
|
||||
return value == sizeof(entry_block);
|
||||
}
|
||||
|
||||
expected<bool, coral::io_error> read(coral::reader & archive_reader) {
|
||||
return archive_reader.read(this->bytes).map<bool>(is_sizeof);
|
||||
}
|
||||
};
|
||||
|
||||
bundle_result bundle(coral::allocator & allocator, fs & output_fs,
|
||||
path const & output_path, fs & input_fs, path const & input_path) {
|
||||
/**
|
||||
* Length of the full magic signature at the beginning of an Oar file.
|
||||
*/
|
||||
constexpr usize header_signature_length {4};
|
||||
|
||||
coral::small_stack<block, 64> archive_blocks {allocator};
|
||||
u64 file_count {0};
|
||||
/**
|
||||
* Hardcoded signature magic value that this implementation of Oar expects when reading archives.
|
||||
*/
|
||||
constexpr u8 header_signature_magic[header_signature_length] {'o', 'a', 'r', 1};
|
||||
|
||||
// Walk input dir to create blocks for all files needed.
|
||||
{
|
||||
bool has_io_error {false};
|
||||
bool is_out_of_memory {false};
|
||||
union header_block {
|
||||
struct {
|
||||
u8 signature[header_signature_length];
|
||||
|
||||
input_fs.walk_files(input_path, [&](file_walker & walker) {
|
||||
while (walker.has_next()) {
|
||||
coral::expected const walked_path {walker.next()};
|
||||
coral::u32 entry_count;
|
||||
} layout;
|
||||
|
||||
if (walked_path.is_error()) {
|
||||
has_io_error = true;
|
||||
u8 bytes[512];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (archive_blocks.push({.layout = {.path = *walked_path.ok()}}) != coral::push_result::ok) {
|
||||
is_out_of_memory = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (has_io_error) return bundle_result::io_error;
|
||||
|
||||
if (is_out_of_memory) return bundle_result::out_of_memory;
|
||||
|
||||
if (file_count > coral::u32_max) return bundle_result::too_many_files;
|
||||
static constexpr bool is_sizeof(usize value) {
|
||||
return value == sizeof(header_block);
|
||||
}
|
||||
|
||||
// Write header, file data, and blocks to archive.
|
||||
{
|
||||
bool has_io_error {false};
|
||||
|
||||
output_fs.write_file(output_path, [&](coral::file_writer & archive_writer) {
|
||||
header archive_header {};
|
||||
|
||||
coral::copy(archive_header.layout.signature, signature_magic);
|
||||
|
||||
// This was safety-checked during the initial file tree walk step.
|
||||
archive_header.layout.entry_count = static_cast<coral::u32>(file_count);
|
||||
|
||||
if (!archive_writer.write(archive_header.bytes).map<bool>(header::is_sizeof).ok_or(false)) {
|
||||
has_io_error = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!archive_blocks.every([&](block & archive_block) -> bool {
|
||||
bool file_read {false};
|
||||
|
||||
input_fs.read_file(archive_block.layout.path, [&](coral::file_reader & entry_reader) {
|
||||
expected const data_position {entry_reader.tell()};
|
||||
|
||||
if (data_position.is_error()) {
|
||||
has_io_error = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
archive_block.layout.data_offset = *data_position.ok();
|
||||
|
||||
{
|
||||
u8 stream_buffer[4096] {0};
|
||||
expected const data_written {coral::stream(archive_writer, entry_reader, stream_buffer)};
|
||||
|
||||
if (data_written.is_error()) {
|
||||
has_io_error = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
archive_block.layout.data_length = *data_written.ok();
|
||||
}
|
||||
|
||||
file_read = true;
|
||||
});
|
||||
|
||||
return file_read && (!has_io_error);
|
||||
})) return;
|
||||
|
||||
if (!archive_blocks.every([&](block const & archive_block) -> bool {
|
||||
if (!archive_writer.write(archive_block.bytes).map<bool>(block::is_sizeof).ok_or(false)) {
|
||||
has_io_error = true;
|
||||
}
|
||||
|
||||
return !has_io_error;
|
||||
})) return;
|
||||
expected<bool, coral::io_error> read(coral::reader & archive_reader) {
|
||||
return archive_reader.read(this->bytes).map<bool>(is_sizeof).map<bool>([&](bool is_valid) -> bool {
|
||||
return is_valid && coral::equals(this->layout.signature, header_signature_magic);
|
||||
});
|
||||
|
||||
if (has_io_error) return bundle_result::io_error;
|
||||
}
|
||||
|
||||
return bundle_result::ok;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
193
source/oar/files.cpp
Normal file
193
source/oar/files.cpp
Normal file
@ -0,0 +1,193 @@
|
||||
export module oar.files;
|
||||
|
||||
import coral;
|
||||
import coral.files;
|
||||
|
||||
import oar;
|
||||
|
||||
using coral::closure;
|
||||
using coral::expected;
|
||||
using coral::file_reader;
|
||||
using coral::file_system;
|
||||
using coral::file_walker;
|
||||
using coral::io_error;
|
||||
using coral::slice;
|
||||
using coral::u64;
|
||||
|
||||
enum class [[nodiscard]] find_result {
|
||||
ok,
|
||||
not_found,
|
||||
io_unavailable,
|
||||
archive_invalid,
|
||||
};
|
||||
|
||||
struct archive_reader : public file_reader {
|
||||
archive_reader(file_reader & archive_file_reader) : archive_file_reader{archive_file_reader} {}
|
||||
|
||||
find_result find(oar::entry_path const & path) {
|
||||
this->data_offset = 0;
|
||||
this->data_length = 0;
|
||||
this->data_cursor = 0;
|
||||
|
||||
if (!this->archive_file_reader.seek(0).is_ok()) return find_result::io_unavailable;
|
||||
|
||||
oar::header_block archive_header_block;
|
||||
|
||||
if (!archive_header_block.read(this->archive_file_reader).ok().or_value(false))
|
||||
return find_result::archive_invalid;
|
||||
|
||||
// Read file table.
|
||||
u64 head {0};
|
||||
u64 tail {archive_header_block.layout.entry_count - 1};
|
||||
oar::entry_block archive_entry_block {};
|
||||
|
||||
while (head <= tail) {
|
||||
u64 const midpoint {head + ((tail - head) / 2)};
|
||||
|
||||
if (!archive_entry_block.read(this->archive_file_reader).ok().or_value(false))
|
||||
return find_result::archive_invalid;
|
||||
|
||||
coral::size const comparison {path.compare(archive_entry_block.layout.path)};
|
||||
|
||||
if (comparison == 0) {
|
||||
this->data_offset = archive_entry_block.layout.data_offset;
|
||||
this->data_length = archive_entry_block.layout.data_length;
|
||||
this->data_cursor = archive_entry_block.layout.data_offset;
|
||||
|
||||
return find_result::ok;
|
||||
}
|
||||
|
||||
if (comparison > 0) {
|
||||
head = (midpoint + 1);
|
||||
} else {
|
||||
tail = (midpoint - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return find_result::not_found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read `data.length` bytes from the file and fill `data` with it, returning the
|
||||
* number of bytes actually read or a [io_error] value to indicate an error occured.
|
||||
*/
|
||||
expected<coral::usize, io_error> read(slice<coral::u8> const & data) override {
|
||||
if (this->data_offset < sizeof(oar::header_block)) return io_error::unavailable;
|
||||
|
||||
coral::usize const data_tail {this->data_offset + this->data_length};
|
||||
|
||||
if (!this->archive_file_reader.seek(coral::clamp(this->data_offset + this->data_cursor,
|
||||
this->data_offset, data_tail)).is_ok()) return io_error::unavailable;
|
||||
|
||||
expected const data_read {this->archive_file_reader.read(
|
||||
data.sliced(0, coral::min(data.length, data_tail - this->data_cursor)))};
|
||||
|
||||
if (data_read.is_ok()) this->data_cursor += *data_read.ok();
|
||||
|
||||
return data_read;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to seek to `offset` absolute position in the file, returning the new absolute
|
||||
* cursor or a [io_error] value to indicate an error occured.
|
||||
*/
|
||||
expected<u64, io_error> seek(u64 offset) override {
|
||||
if (this->data_offset < sizeof(oar::header_block)) return io_error::unavailable;
|
||||
|
||||
this->data_cursor = offset;
|
||||
|
||||
return io_error::unavailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read to read the absolute file cursor position, returning it or a [io_error]
|
||||
* value to indicate an error occured.
|
||||
*/
|
||||
expected<u64, io_error> tell() override {
|
||||
if (this->data_offset < sizeof(oar::header_block)) return io_error::unavailable;
|
||||
|
||||
return this->data_cursor;
|
||||
}
|
||||
|
||||
private:
|
||||
file_reader & archive_file_reader;
|
||||
|
||||
u64 data_offset {0};
|
||||
|
||||
u64 data_length {0};
|
||||
|
||||
u64 data_cursor {0};
|
||||
};
|
||||
|
||||
struct archive_walker : public file_walker {
|
||||
archive_walker(file_reader & archive_file_reader) : archive_file_reader{archive_file_reader} {}
|
||||
|
||||
bool enumerate() override {
|
||||
// TODO: implement.
|
||||
return false;
|
||||
}
|
||||
|
||||
find_result find(oar::entry_path const & path) {
|
||||
// TODO: implement.
|
||||
return find_result::not_found;
|
||||
}
|
||||
|
||||
expected<slice<char const>, io_error> value() override {
|
||||
// TODO: implement.
|
||||
return io_error::unavailable;
|
||||
}
|
||||
|
||||
private:
|
||||
file_reader & archive_file_reader;
|
||||
|
||||
u64 files_enumerated {0};
|
||||
};
|
||||
|
||||
export namespace oar {
|
||||
struct archive_file : public file_system {
|
||||
static void read(file_system & base_file_system, slice<char const> const & archive_path, closure<void(archive_file &)> const & then) {
|
||||
if (base_file_system.query_file(archive_path).is_error()) return;
|
||||
|
||||
archive_file file {base_file_system, archive_path};
|
||||
|
||||
then(file);
|
||||
}
|
||||
|
||||
void read_file(slice<char const> const & path, closure<void(file_reader &)> const & then) override {
|
||||
this->base_file_system.read_file(this->archive_path, [&](file_reader & archive_file_reader) {
|
||||
expected const parsed_entry_path {entry_path::parse(path)};
|
||||
|
||||
if (parsed_entry_path.is_error()) return;
|
||||
|
||||
archive_reader reader {archive_file_reader};
|
||||
|
||||
if (reader.find(*parsed_entry_path.ok()) != find_result::ok) return;
|
||||
|
||||
then(reader);
|
||||
});
|
||||
}
|
||||
|
||||
void walk_files(slice<char const> const & path, closure<void(file_walker &)> const & then) override {
|
||||
this->base_file_system.read_file(this->archive_path, [&](file_reader & archive_file_reader) {
|
||||
expected const parsed_entry_path {entry_path::parse(path)};
|
||||
|
||||
if (parsed_entry_path.is_error()) return;
|
||||
|
||||
archive_walker walker {archive_file_reader};
|
||||
|
||||
if (walker.find(*parsed_entry_path.ok()) != find_result::ok) return;
|
||||
|
||||
then(walker);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
file_system & base_file_system;
|
||||
|
||||
slice<char const> archive_path;
|
||||
|
||||
archive_file(file_system & base_file_system, slice<char const> const & archive_path) : base_file_system{base_file_system} {
|
||||
this->archive_path = archive_path;
|
||||
}
|
||||
};
|
||||
}
|
@ -1,55 +1,60 @@
|
||||
export module runtime;
|
||||
|
||||
import app;
|
||||
|
||||
import coral;
|
||||
import coral.files;
|
||||
import coral.io;
|
||||
import coral.math;
|
||||
import coral.stack;
|
||||
|
||||
import oar.files;
|
||||
|
||||
import turtle;
|
||||
import turtle.io;
|
||||
|
||||
extern "C" int main(int argc, char const * const * argv) {
|
||||
return app::client::run("Ona Runtime", [](app::client & client) -> int {
|
||||
constexpr coral::path config_path{"config.kym"};
|
||||
bool is_config_loaded{false};
|
||||
turtle::event_loop::run("Ona Runtime", [](turtle::event_loop & event_loop) -> void {
|
||||
constexpr coral::slice archive_path {"index.oar"};
|
||||
bool is_archive_loaded {false};
|
||||
|
||||
client.resources().read_file(config_path, [&](coral::reader & file) {
|
||||
coral::allocator * const allocator{&client.thread_safe_allocator()};
|
||||
oar::archive_file::read(turtle::file_sandbox::base(), archive_path, [&](oar::archive_file & archive_file) -> void {
|
||||
is_archive_loaded = true;
|
||||
|
||||
coral::small_stack<coral::u8> script_source{allocator};
|
||||
{
|
||||
coral::u8 stream_buffer[1024]{0};
|
||||
coral::stack_writer script_writer{&script_source};
|
||||
constexpr coral::slice config_path {"config.kym"};
|
||||
bool is_config_loaded {false};
|
||||
|
||||
if (!coral::stream(script_writer, file, stream_buffer).is_ok()) return;
|
||||
archive_file.read_file(config_path, [&](coral::file_reader & config_reader) {
|
||||
archive_file.query_file(config_path).and_then([&](coral::file_system::file_info file_info) {
|
||||
turtle::system_allocator allocator {turtle::system_allocator::thread_safety::none};
|
||||
coral::ring_buffer config_source;
|
||||
|
||||
if (config_source.allocate(allocator, file_info.size) != coral::ring_buffer::allocate_result::ok)
|
||||
return;
|
||||
|
||||
coral::u8 stream_buffer[1024] {0};
|
||||
|
||||
if (coral::stream(config_source, config_reader, stream_buffer).is_error()) return;
|
||||
|
||||
is_config_loaded = true;
|
||||
});
|
||||
});
|
||||
|
||||
if (!is_config_loaded) {
|
||||
static_cast<void>(coral::println(turtle::error_log(), "failed to read", config_path));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
client.log(app::log_level::notice, script_source.as_slice().as_chars());
|
||||
// event_loop.display(1280, 800);
|
||||
|
||||
is_config_loaded = true;
|
||||
while (event_loop.poll()) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (!is_config_loaded) {
|
||||
coral::small_stack<coral::u8> error_message{&client.thread_safe_allocator()};
|
||||
{
|
||||
coral::stack_writer error_writer{&error_message};
|
||||
if (!is_archive_loaded) {
|
||||
static_cast<void>(coral::println(turtle::error_log(), "failed to open", archive_path));
|
||||
|
||||
if (!error_writer.write(coral::slice{"failed to load "}.as_bytes()).is_ok())
|
||||
return coral::u8_max;
|
||||
|
||||
if (!error_writer.write(config_path.as_slice().as_bytes()).is_ok())
|
||||
return coral::u8_max;
|
||||
}
|
||||
|
||||
return coral::u8_max;
|
||||
return;
|
||||
}
|
||||
|
||||
client.display(1280, 800);
|
||||
|
||||
while (client.poll()) {
|
||||
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import coral.io;
|
||||
|
||||
using coral::closure;
|
||||
using coral::io_error;
|
||||
using coral::path;
|
||||
using coral::slice;
|
||||
using coral::unreachable;
|
||||
using coral::usize;
|
||||
@ -336,126 +335,10 @@ export namespace turtle {
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides unmanaged access to a native directory.
|
||||
* [coral::file_system] wrapper around native file system access to provide a managed and system-agnostic
|
||||
* environment for performing file I/O.
|
||||
*/
|
||||
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 {
|
||||
struct file_sandbox : public coral::file_system {
|
||||
/**
|
||||
* Permission flags that a [sandboxed_fs] may specify for restricting access to it.
|
||||
*/
|
||||
@ -468,16 +351,15 @@ export namespace turtle {
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs a sandbox located at `sandbox_path` with `sandbox_permissions` as the permissions given to users
|
||||
* of it.
|
||||
* Constructs a sandbox located at `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;
|
||||
file_sandbox(native_path const & path, permissions const & access_permissions) {
|
||||
this->path = 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
|
||||
* Returns a reference to a [file_sandbox] 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-
|
||||
@ -485,17 +367,17 @@ export namespace turtle {
|
||||
*
|
||||
* *Note*: The base file system does not permit being written to.
|
||||
*/
|
||||
static sandboxed_fs & base() {
|
||||
static sandboxed_fs base_fs {"./", {
|
||||
static file_sandbox & base() {
|
||||
static file_sandbox base_sandbox {"./", {
|
||||
.can_read = true,
|
||||
.can_walk = true
|
||||
}};
|
||||
|
||||
return base_fs;
|
||||
return base_sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to a [sandboxed_fs] that operates as a temporary file store.
|
||||
* Returns a reference to a [file_sandbox] 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
|
||||
@ -503,13 +385,13 @@ export namespace turtle {
|
||||
*
|
||||
* *Note*: The temp file system does not permit being walked.
|
||||
*/
|
||||
static sandboxed_fs & temp() {
|
||||
static sandboxed_fs base_fs {"/tmp", {
|
||||
static file_sandbox & temp() {
|
||||
static file_sandbox temp_sandbox {"/tmp", {
|
||||
.can_read = true,
|
||||
.can_write = false,
|
||||
}};
|
||||
|
||||
return base_fs;
|
||||
return temp_sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -517,17 +399,16 @@ export namespace turtle {
|
||||
*
|
||||
* *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 {
|
||||
void read_file(slice<char const> const & 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 {
|
||||
this->path.joined(path).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;
|
||||
});
|
||||
}
|
||||
@ -537,18 +418,11 @@ export namespace turtle {
|
||||
*
|
||||
* *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 {
|
||||
void walk_files(slice<char const> const & 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;
|
||||
this->path.joined(path).and_then([&](native_path const & native_directory_path) -> void {
|
||||
// TODO: Implement.
|
||||
});
|
||||
}
|
||||
|
||||
@ -557,17 +431,16 @@ export namespace turtle {
|
||||
*
|
||||
* *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 {
|
||||
void write_file(slice<char const> const & 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 {
|
||||
this->path.joined(path).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;
|
||||
});
|
||||
}
|
||||
@ -583,12 +456,9 @@ export namespace turtle {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns a reference to the process-wide error logging [coral::writer] used for recording errors.
|
||||
*/
|
||||
coral::writer & output() {
|
||||
coral::writer & error_log() {
|
||||
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)};
|
||||
|
@ -13,33 +13,35 @@ export namespace turtle {
|
||||
};
|
||||
|
||||
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) {
|
||||
static void run(coral::slice<char const> const & title, coral::closure<void(event_loop &)> execute) {
|
||||
event_loop loop{title};
|
||||
|
||||
return execute(loop);
|
||||
}
|
||||
|
||||
private:
|
||||
coral::path title;
|
||||
static constexpr coral::usize title_max = 256;
|
||||
|
||||
event_loop(coral::path const & title) {
|
||||
this->title = title;
|
||||
coral::u8 title_buffer[256];
|
||||
|
||||
event_loop(coral::slice<char const> const & title) {
|
||||
coral::copy(this->title_buffer, title.sliced(0, coral::min(title.length, title_max)).as_bytes());
|
||||
}
|
||||
};
|
||||
|
||||
struct system_allocator : public coral::allocator {
|
||||
system_allocator() = default;
|
||||
enum class thread_safety {
|
||||
none,
|
||||
mutex,
|
||||
};
|
||||
|
||||
// TODO: implement thread-safety.
|
||||
system_allocator(thread_safety allocator_thread_safety) {
|
||||
// TODO: optimize allocator with thread-unsafe variant.
|
||||
}
|
||||
|
||||
coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override {
|
||||
if (maybe_allocation != nullptr) coral::unreachable();
|
||||
|
Loading…
x
Reference in New Issue
Block a user