From 13fffacd98ea6aade51cff5c14e7bc17ac3bcdbd Mon Sep 17 00:00:00 2001 From: kayomn Date: Sun, 26 Feb 2023 18:34:57 +0000 Subject: [PATCH] Add way to bundle files in a coral::fs into an Ona archive --- source/coral.cpp | 174 ++++++++++++++----- source/coral/files.cpp | 23 +-- source/coral/io.cpp | 18 +- source/coral/stack.cpp | 100 +++++++++-- source/oar.cpp | 384 ++++++++++++++++++++++++++++------------- 5 files changed, 499 insertions(+), 200 deletions(-) diff --git a/source/coral.cpp b/source/coral.cpp index 40875ae..ad03759 100644 --- a/source/coral.cpp +++ b/source/coral.cpp @@ -40,6 +40,8 @@ export namespace coral { using u32 = uint32_t; + usize const u32_max = 0xffffffff; + using i32 = int32_t; usize const i32_max = 0xffffffff; @@ -351,83 +353,160 @@ export namespace coral { result(* dispatch)(void *, arguments...); }; + /** + * Monadic container for a single-`element` value or nothing. + */ + template struct [[nodiscard]] optional { + /** + * Constructs an empty [optional]. + */ + constexpr optional() = default; + + /** + * Constructs an [optional] that contains `value`. + */ + constexpr optional(element const & value) { + (*reinterpret_cast(this->buffer)) = value; + this->buffer[sizeof(element)] = 1; + } + + /** + * Constructs an [optional] from `that`, copying over its data. + */ + constexpr optional(optional const & that) { + if (that.has_value()) { + (*reinterpret_cast(this->buffer)) = *that; + this->buffer[sizeof(element)] = 1; + } else { + this->buffer[sizeof(element)] = 0; + } + } + + /** + * Returns `true` if the optional contains a value, otherwise `false`. + */ + bool has_value() const { + return this->buffer[sizeof(element)] == 1; + } + + /** + * Monadically maps `apply` to the value if it exists, otherwise doing nothing. + */ + template optional map(closure const & apply) const { + if (this->has_value()) return apply(this->value()); + + return {}; + } + + /** + * Returns the contained value or `fallback` if the optional is empty. + */ + element const & or_value(element const & fallback) const { + return this->has_value() ? *reinterpret_cast(this->buffer) : fallback; + } + + /** + * Returns a reference to the contained value. + * + * *Note*: attempting to access the value of an empty optional will trigger safety-checked behavior. + */ + element & operator *() { + if (!this->has_value()) unreachable(); + + return *reinterpret_cast(this->buffer); + } + + /** + * Returns a const reference to the contained value. + * + * *Note*: attempting to access the value of an empty optional will trigger safety-checked behavior. + */ + element const & operator *() const { + if (!this->has_value()) unreachable(); + + return *reinterpret_cast(this->buffer); + } + + private: + u8 buffer[sizeof(element) + 1] {0}; + }; + + /** * Monadic container for a descriminating union of either `expects` or `errors`. */ template struct [[nodiscard]] expected { - expected(expects const & value) : buffer{0} { + template using rebound = expected; + + /** + * Constructs from `value`, creating an [expected] that contains the expected type. + */ + expected(expects const & value) { (*reinterpret_cast(this->buffer)) = value; this->buffer[buffer_size] = 1; } - expected(errors const & error) : buffer{0} { + /** + * Constructs from `error`, creating an [expected] that does not contain the expected type. + */ + expected(errors const & error) { (*reinterpret_cast(this->buffer)) = error; } + /** + * Returns the contained error as an [optional]. + */ + optional error() const { + if (this->is_error()) return *reinterpret_cast(this->buffer); + + return {}; + } + + /** + * Returns `true` if the optional holds an error, otherwise `false` if it is ok. + */ + bool is_error() const { + return this->buffer[buffer_size] == 0; + } + /** * Returns `true` if the optional contains the expected value, otherwise `false` if it holds an error. */ bool is_ok() const { - return this->buffer[buffer_size]; + return this->buffer[buffer_size] == 1; } /** - * Returns a reference to the contained value. - * - * *Note*: attempting to access the value of an erroneous expected will trigger safety-checked behavior. + * Monadically maps `apply` to the value if it exists, otherwise doing nothing. */ - expects & value() { - if (!this->is_ok()) unreachable(); + template rebound map(closure const & apply) const { + if (this->is_ok()) return apply(*this->ok()); - return *reinterpret_cast(this->buffer); + return *this->error(); } /** - * Returns the contained value. - * - * *Note*: attempting to access the value of an erroneous expected will trigger safety-checked behavior. + * Returns the contained ok value as an [optional]. */ - expects const & value() const { - if (!this->is_ok()) unreachable(); + optional ok() const { + if (this->is_ok()) return *reinterpret_cast(this->buffer); - return *reinterpret_cast(this->buffer); + return {}; } /** - * Returns a reference to the contained error. - * - * *Note*: attempting to access the error of a non-erroneous expected will trigger safety-checked behavior. + * Returns the contained value or `value` if it is not ok. */ - errors & error() { - if (this->is_ok()) unreachable(); + expects ok_or(expects value) const { + if (this->is_ok()) return *this->ok(); - return *reinterpret_cast(this->buffer); - } - - /** - * Returns the contained error. - * - * *Note*: attempting to access the error of a non-erroneous expected will trigger safety-checked behavior. - */ - errors const & error() const { - if (this->is_ok()) unreachable(); - - return *reinterpret_cast(this->buffer); - } - - /** - * - */ - template expected map(closure const & apply) const { - if (this->is_ok()) return apply(this->value()); - - return this->error(); + return value; } private: static constexpr usize buffer_size = max(sizeof(expects), sizeof(errors)); - u8 buffer[buffer_size + 1]; + u8 buffer[buffer_size + 1] {0}; }; /** @@ -514,11 +593,20 @@ export namespace coral { constexpr bool equals(slice const & a, slice const & b) { if (a.length != b.length) return false; - for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false; + for (usize i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false; return true; } + /** + * Performs a linear search from the back of + */ + constexpr optional find_last(slice const & bytes, u8 byte) { + for (usize i = bytes.length; i >= 0; i -= 1) if (bytes[i] == byte) return i; + + return {}; + } + /** * Returns a hash code generated from the values in `bytes`. * diff --git a/source/coral/files.cpp b/source/coral/files.cpp index 588ff00..b41e2e0 100644 --- a/source/coral/files.cpp +++ b/source/coral/files.cpp @@ -153,16 +153,19 @@ export namespace coral { bool can_write; }; + enum class [[nodiscard]] walk_result { + ok, + not_implemented, + access_denied, + not_found, + io_error, + }; + virtual ~fs() {}; - /** - * Attempts to read the files in `directory_path`, calling `apply` for each of the files encountered with the - * fully-qualified file path. If either no files are found or the file-system does not support the operation, - * `apply` is never caled. - * - * `false` may be returned inside of `apply` to halt the enumeration. - */ - virtual void enumerate_directory(path const & directory_path, closure const & apply) {} + virtual walk_result walk_files(path const & target_path, closure const & apply) { + return walk_result::not_implemented; + } /** * Queries the file-system for its global [access_rules], returning them. @@ -172,12 +175,12 @@ export namespace coral { /** * Attempts to read the file in `file_path`, calling `then` if it was successfully opened for reading. */ - virtual void read_file(path const & file_path, closure const & then) {} + virtual void read_file(path const & target_path, closure const & then) {} /** * Attempts to write the file in the file system located at `file_path`, calling `then` if it was successfully * opened for writing. */ - virtual void write_file(path const & file_path, closure const & then) {} + virtual void write_file(path const & target_path, closure const & then) {} }; } diff --git a/source/coral/io.cpp b/source/coral/io.cpp index 11221e8..8741307 100644 --- a/source/coral/io.cpp +++ b/source/coral/io.cpp @@ -123,25 +123,25 @@ export namespace coral { * *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to temporarily place data * during streaming. */ - expected stream(writer & output, reader & input, slice const & buffer) { - usize total_bytes_written = 0; - expected bytes_read = input.read(buffer); + expected stream(writer & output, reader & input, slice const & buffer) { + u64 total_bytes_written {0}; + expected bytes_read {input.read(buffer)}; - if (!bytes_read.is_ok()) return bytes_read.error(); + if (bytes_read.is_error()) return *bytes_read.error(); - usize read = bytes_read.value(); + usize read {*bytes_read.ok()}; while (read != 0) { expected const bytes_written = output.write(buffer.sliced(0, read)); - if (!bytes_written.is_ok()) return bytes_read.error(); + if (bytes_written.is_error()) return *bytes_read.error(); - total_bytes_written += bytes_written.value(); + total_bytes_written += *bytes_written.ok(); bytes_read = input.read(buffer); - if (!bytes_read.is_ok()) return bytes_read.error(); + if (bytes_read.is_error()) return *bytes_read.error(); - read = bytes_read.value(); + read = *bytes_read.ok(); } return total_bytes_written; diff --git a/source/coral/stack.cpp b/source/coral/stack.cpp index 12707ad..4be80ed 100644 --- a/source/coral/stack.cpp +++ b/source/coral/stack.cpp @@ -26,12 +26,31 @@ export namespace coral { virtual ~stack() {}; /** - * Returns a read-only [slice] of the current range values. - * - * *Note*: the behavior of retaining the returned value past the scope of the source [stack] or any subsequent - * modifications to it is implementation-defined. + * Returns `true` if there are no elements in the stack, otherwise `false`. */ - virtual slice as_slice() const = 0; + virtual bool is_empty() const = 0; + + /** + * Invokes `apply` on each element in the stack and evaluating a user-defined condition which will return a + * `bool` at the end of each evaluation. After all elements have been evaluated, `true` is returned if every + * element elavuated `true`. Otherwise, `false` is returned to indicate that one of more elements failed + * evaluation. + * + * *Note*: This function uses short-circuit evaluation, so the enumeration will terminate upon the first failure + * case. This may be leveraged to create conditional looping behavior. + */ + virtual bool every(closure apply) = 0; + + /** + * Invokes `apply` on each element in the stack and evaluating a user-defined condition which will return a + * `bool` at the end of each evaluation. After all elements have been evaluated, `true` is returned if every + * element elavuated `true`. Otherwise, `false` is returned to indicate that one of more elements failed + * evaluation. + * + * *Note*: This function uses short-circuit evaluation, so the enumeration will terminate upon the first failure + * case. This may be leveraged to create conditional looping behavior. + */ + virtual bool every(closure apply) const = 0; /** * Attempts to append `source_elements` to the stack. @@ -55,28 +74,60 @@ export namespace coral { * *Note*: the [allocator] referenced in the stack must remain valid for the duration of the stack lifetime. */ template struct small_stack : public stack { - small_stack(allocator * dynamic_allocator) { - this->dynamic_allocator = dynamic_allocator; - } + small_stack(allocator & dynamic_allocator) : dynamic_allocator{dynamic_allocator} {} ~small_stack() override { if (this->is_dynamic()) { for (element & e : this->elements) e.~element(); - this->dynamic_allocator->deallocate(this->elements.pointer); + this->dynamic_allocator.deallocate(this->elements.pointer); } } /** - * Returns a read-only [slice] of the current stack values. + * Returns a const [slice] of the current stack values. * * *Note*: the returned slice should be considered invalid if any mutable operation is performed on the source * [stack] or it is no longer in scope. */ - slice as_slice() const override { + slice as_slice() const { return this->elements.sliced(0, this->filled); } + /** + * Invokes `apply` on each element in the stack and evaluating a user-defined condition which will return a + * `bool` at the end of each evaluation. After all elements have been evaluated, `true` is returned if every + * element elavuated `true`. Otherwise, `false` is returned to indicate that one of more elements failed + * evaluation. + * + * *Note*: This function uses short-circuit evaluation, so the enumeration will terminate upon the first failure + * case. This may be leveraged to create conditional looping behavior. + */ + bool every(closure apply) override { + for (usize index = 0; index < this->filled; index += 1) { + if (!apply(this->elements[index])) return false; + } + + return true; + } + + /** + * Invokes `apply` on each element in the stack and evaluating a user-defined condition which will return a + * `bool` at the end of each evaluation. After all elements have been evaluated, `true` is returned if every + * element elavuated `true`. Otherwise, `false` is returned to indicate that one of more elements failed + * evaluation. + * + * *Note*: This function uses short-circuit evaluation, so the enumeration will terminate upon the first failure + * case. This may be leveraged to create conditional looping behavior. + */ + bool every(closure apply) const override { + for (usize index = 0; index < this->filled; index += 1) { + if (!apply(this->elements[index])) return false; + } + + return true; + } + /** * Returns `true` if the stack is backed by dynamic memory, otherwise `false`. */ @@ -84,6 +135,13 @@ export namespace coral { return this->elements.pointer != reinterpret_cast(this->local_buffer); } + /** + * Returns `true` if there are no elements in the stack, otherwise `false`. + */ + bool is_empty() const override { + return this->filled == 0; + } + /** * Attempts to append `source_element` to the top of the stack. * @@ -149,7 +207,7 @@ export namespace coral { usize const requested_capacity = this->filled + capacity; if (this->is_dynamic()) { - u8 * const buffer = this->dynamic_allocator->reallocate( + u8 * const buffer = this->dynamic_allocator.reallocate( reinterpret_cast(this->elements.pointer), sizeof(element) * requested_capacity); @@ -162,7 +220,7 @@ export namespace coral { this->elements = {reinterpret_cast(buffer), requested_capacity}; } else { usize const buffer_size = sizeof(element) * requested_capacity; - u8 * const buffer = this->dynamic_allocator->reallocate(nullptr, buffer_size); + u8 * const buffer = this->dynamic_allocator.reallocate(nullptr, buffer_size); if (buffer == nullptr) { this->elements = {}; @@ -179,7 +237,7 @@ export namespace coral { } private: - allocator * dynamic_allocator{nullptr}; + allocator & dynamic_allocator; usize filled{0}; @@ -206,14 +264,18 @@ export namespace coral { * Reads the data from the target stack into `buffer`, returning the number bytes read. */ expected read(slice const & buffer) override { - slice const stack_elements {this->stack->as_slice()}; - usize const read {min(buffer.length, stack_elements.length - this->cursor)}; + usize data_written = 0; - copy(buffer, stack_elements.sliced(cursor, read)); + this->stack->every([&](u8 byte) -> bool { + buffer[data_written] = byte; + data_written += 1; - this->cursor += read; + return data_written < buffer.length; + }); - return read; + this->cursor += data_written; + + return data_written; } private: diff --git a/source/oar.cpp b/source/oar.cpp index a60a902..b984a1f 100644 --- a/source/oar.cpp +++ b/source/oar.cpp @@ -2,137 +2,151 @@ 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::fs; +using coral::io_error; +using coral::path; +using coral::u8; +using coral::u64; +using coral::usize; /** * Length of the full magic signature at the beginning of an Oar file. */ -constexpr coral::usize signature_length {4}; - -/** - * Length of the magic signature at the beginning of an Oar file without the version indicator - * byte. - */ -constexpr coral::usize signature_identifier_length {signature_length - 1}; +constexpr static usize signature_length {4}; /** * Hardcoded signature magic value that this implementation of Oar expects when reading archives. */ -constexpr coral::u8 signature_magic[signature_length] {'o', 'a', 'r', 0}; +constexpr static u8 signature_magic[signature_length] {'o', 'a', 'r', 1}; /** * Oar file header format. */ -struct header { - coral::u8 signature_magic[signature_length]; +union header { + struct { + u8 signature[signature_length]; - coral::u32 entry_count; + coral::u32 entry_count; + } layout; - coral::u8 padding[504]; + u8 bytes[512]; + + static constexpr bool is_sizeof(usize value) { + return value == sizeof(header); + } + + expected read(coral::reader & archive_reader) { + return archive_reader.read(this->bytes).map(is_sizeof).map([&](bool is_valid) -> bool { + return is_valid && coral::equals(this->layout.signature, signature_magic); + }); + } }; -static_assert(sizeof(header) == 512); +static_assert(header::is_sizeof(512)); -/** - * Oar file header format. - */ -struct entry { - coral::path path; - - coral::u64 data_offset; - - coral::u64 data_length; - - coral::u8 padding[240]; +enum class entry_kind { + file, + directory, }; -static_assert(sizeof(entry) == 512); +/** + * 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 read(coral::reader & archive_reader) { + return archive_reader.read(this->bytes).map(is_sizeof); + } +}; + +static_assert(block::is_sizeof(512)); /** - * Archive file access interface. + * Archive entry access interface. */ -struct archive_file : public coral::file_reader { +struct entry : 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 - * [coral::file_reader] for whatever reason. + * [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 [coral::file_reader] is not - * reading from an Oar archive file. + * [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::archive_unsupported] signals that data was read and was formatted as expected - * for an Oar archive, however, it is from an unsupported version of the archive format. - * - * [find_result::not_found] indicates that no entry in the archive could be found that matches - * the given query. + * [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, - archive_unsupported, not_found, }; - archive_file(coral::file_reader * archive_reader) { + 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. + * 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(coral::path const & file_path) { + 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; - constexpr coral::usize header_size {sizeof(header)}; - coral::u8 archive_header_buffer[header_size] {0}; + header archive_header {}; - if (!this->archive_reader->read(archive_header_buffer).and_test( - [](coral::usize value) -> bool { return value == header_size; })) - return find_result::archive_invalid; - - header const * const archive_header { - reinterpret_cast
(archive_header_buffer)}; - - if (!coral::equals({archive_header->signature_magic, signature_identifier_length}, - {signature_magic, signature_identifier_length})) return find_result::archive_invalid; - - if (archive_header->signature_magic[signature_identifier_length] != - signature_magic[signature_identifier_length]) return find_result::archive_unsupported; + if (!archive_header.read(*this->archive_reader).map(coral::equality_predicate(true)).is_ok()) + return find_result::archive_invalid; // Read file table. - coral::u64 head {0}; - coral::u64 tail {archive_header->entry_count - 1}; - constexpr coral::usize entry_size {sizeof(entry)}; - coral::u8 archive_entry_buffer[entry_size] {0}; + u64 head {0}; + u64 tail {archive_header.layout.entry_count - 1}; + block archive_block {}; while (head <= tail) { - coral::u64 const midpoint {head + ((tail - head) / 2)}; + u64 const midpoint {head + ((tail - head) / 2)}; - if (!this->archive_reader->seek(header_size + (entry_size * midpoint)).is_ok()) + if (!archive_block.read(*this->archive_reader).map(coral::equality_predicate(true)).is_ok()) return find_result::archive_invalid; - if (!this->archive_reader->read(archive_entry_buffer).and_test( - [](coral::usize value) -> bool { return value == entry_size; })) - return find_result::archive_invalid; + if (archive_block.layout.kind == kind) return find_result::not_found; - entry const * const archive_entry { - reinterpret_cast(archive_entry_buffer)}; - - coral::size const comparison {file_path.compare(archive_entry->path)}; + coral::size const comparison {entry_path.compare(archive_block.layout.path)}; if (comparison == 0) { - this->data_offset = archive_entry->data_offset; - this->data_length = archive_entry->data_length; - this->data_cursor = archive_entry->data_offset; + 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; } @@ -149,65 +163,115 @@ struct archive_file : public coral::file_reader { /** * Attempts to read `data.length` bytes from the file and fill `data` with it, returning the - * number of bytes actually read or a [coral::io_error] value to indicate an error occured. + * number of bytes actually read or a [io_error] value to indicate an error occured. */ - coral::expected read(coral::slice const & data) override { - if (this->data_offset < sizeof(header)) return coral::io_error::unavailable; + expected read(coral::slice const & data) override { + if (this->data_offset < sizeof(header)) return io_error::unavailable; - coral::usize const data_tail {this->data_offset + this->data_length}; + 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 coral::io_error::unavailable; + this->data_offset, data_tail)).is_ok()) return io_error::unavailable; - coral::expected const data_read {this->archive_reader->read( + 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.value(); + 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 [coral::io_error] value to indicate an error occured. + * cursor or a [io_error] value to indicate an error occured. */ - coral::expected seek(coral::u64 offset) override { - if (this->data_offset < sizeof(header)) return coral::io_error::unavailable; + expected seek(u64 offset) override { + if (this->data_offset < sizeof(header)) return io_error::unavailable; this->data_cursor = offset; - return coral::io_error::unavailable; + return io_error::unavailable; } /** - * Attempts to read to read the absolute file cursor position, returning it or a - * [coral::io_error] value to indicate an error occured. + * Attempts to read to read the absolute file cursor position, returning it or a [io_error] + * value to indicate an error occured. */ - coral::expected tell() override { - if (this->data_offset < sizeof(header)) return coral::io_error::unavailable; + expected tell() override { + if (this->data_offset < sizeof(header)) return io_error::unavailable; return this->data_cursor; } private: - coral::file_reader * archive_reader {nullptr}; + file_reader * archive_reader {nullptr}; - coral::u64 data_offset {0}; + u64 data_offset {0}; - coral::u64 data_length {0}; + u64 data_length {0}; - coral::u64 data_cursor {0}; + u64 data_cursor {0}; }; export namespace oar { - struct archive : public coral::fs { - archive(coral::fs * backing_fs, coral::path const & archive_path) { + struct archive : public fs { + archive(fs * backing_fs, path const & archive_path) { this->backing_fs = backing_fs; this->archive_path = archive_path; } + walk_result walk_files(path const & target_path, closure const & apply) override { + bool not_found {false}; + bool has_io_error {false}; + + 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) { + not_found = true; + + return; + } + + for (;;) { + constexpr usize path_size {sizeof(path)}; + u8 path_buffer[path_size] {0}; + expected const data_read {archive_entry.read(path_buffer)}; + + if (data_read.is_error()) { + has_io_error = true; + + return; + } + + if (usize const data_read_value {*data_read.ok()}; data_read_value != path_size) { + if (data_read_value != 0) has_io_error = true; + + return; + } + + // Verify existence of zero terminator in path. + if (!coral::find_last(path_buffer, 0).has_value()) { + has_io_error = true; + + return; + } + + if (archive_entry.read(path_buffer).map(coral::equality_predicate(path_size)).ok_or(false)) + + if (!apply(*reinterpret_cast(path_buffer))) return; + } + }); + + if (not_found) return walk_result::not_found; + + if (has_io_error) return walk_result::io_error; + + return walk_result::ok; + } + /** - * Queries the archive for the [coral::fs::access_rules] and returns them. + * Queries the archive for the [fs::access_rules] and returns them. */ access_rules query_access() override { return { @@ -217,37 +281,119 @@ export namespace oar { } /** - * Attempts to open a readable context for reading from the archive file identified by - * `file_path`, doing nothing if the requested file could not be found. + * Attempts to open a readable context for reading from the archive file identified by `file_path`, doing + * nothing if the requested file could not be found. */ - void read_file(coral::path const & file_path, - coral::closure const & then) override { + void read_file(path const & file_path, closure const & then) override { + if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return; - if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return; + this->backing_fs->read_file(this->archive_path, [&](file_reader & archive_reader) { + entry archive_entry {&archive_reader}; - this->backing_fs->read_file(this->archive_path, - [&](coral::file_reader & archive_reader) { - archive_file file{&archive_reader}; + if (archive_entry.find(entry_kind::file, file_path) != entry::find_result::ok) return; - if (file.find(file_path) != archive_file::find_result::ok) return; - - then(file); - }); - } - - /** - * Attempts to open a writable context for reading from the archive file identified by - * `file_path`, however this will always do nothing as archive file-systems are read-only. - */ - void write_file(coral::path const & file_path, - coral::closure const & then) override { - - // Read-only file system. - } + then(archive_entry); + }); + } private: - coral::fs * backing_fs; + fs * backing_fs; - coral::path archive_path; + path archive_path; }; + + enum class [[nodiscard]] bundle_result { + ok, + out_of_memory, + too_many_files, + io_error, + }; + + bundle_result bundle(coral::allocator & allocator, fs & output_fs, + path const & output_path, fs & input_fs, path const & input_path) { + + coral::small_stack archive_blocks {allocator}; + u64 file_count {0}; + + // Walk input dir to create blocks for all files needed. + { + bool has_memory {true}; + + if (input_fs.walk_files(input_path, [&](path const & entry_path) -> bool { + has_memory = archive_blocks.push({.layout = {.path = entry_path}}) == coral::push_result::ok; + + return !has_memory; + + file_count += 1; + }) != fs::walk_result::ok) return bundle_result::io_error; + + if (!has_memory) return bundle_result::out_of_memory; + + if (file_count > coral::u32_max) return bundle_result::too_many_files; + } + + // 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); + + archive_header.layout.entry_count = static_cast(file_count); + + if (!archive_writer.write(archive_header.bytes).map(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(block::is_sizeof).ok_or(false)) { + has_io_error = true; + } + + return !has_io_error; + })) return; + }); + + if (has_io_error) return bundle_result::io_error; + } + + return bundle_result::ok; + } }