export module oar; import coral; import coral.files; /** * 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}; /** * Hardcoded signature magic value that this implementation of Oar expects when reading archives. */ constexpr coral::u8 signature_magic[signature_length] {'o', 'a', 'r', 0}; /** * Oar file header format. */ struct header { coral::u8 signature_magic[signature_length]; coral::u32 entry_count; coral::u8 padding[504]; }; static_assert(sizeof(header) == 512); /** * Oar file header format. */ struct entry { coral::path path; coral::u64 data_offset; coral::u64 data_length; coral::u8 padding[240]; }; static_assert(sizeof(entry) == 512); /** * Archive file access interface. */ struct archive_file : public coral::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::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_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. */ enum class [[nodiscard]] find_result { ok, io_unavailable, archive_invalid, archive_unsupported, not_found, }; archive_file(coral::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(coral::path const & file_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}; 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; // 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}; while (head <= tail) { coral::u64 const midpoint {head + ((tail - head) / 2)}; if (!this->archive_reader->seek(header_size + (entry_size * midpoint)).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; entry const * const archive_entry { reinterpret_cast(archive_entry_buffer)}; coral::size const comparison {file_path.compare(archive_entry->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; 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 [coral::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; coral::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; coral::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(); 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. */ coral::expected seek(coral::u64 offset) override { if (this->data_offset < sizeof(header)) return coral::io_error::unavailable; this->data_cursor = offset; return coral::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. */ coral::expected tell() override { if (this->data_offset < sizeof(header)) return coral::io_error::unavailable; return this->data_cursor; } private: coral::file_reader * archive_reader {nullptr}; coral::u64 data_offset {0}; coral::u64 data_length {0}; coral::u64 data_cursor {0}; }; export namespace oar { struct archive : public coral::fs { archive(coral::fs * backing_fs, coral::path const & archive_path) { this->backing_fs = backing_fs; this->archive_path = archive_path; } /** * Queries the archive for the [coral::fs::access_rules] and returns them. */ access_rules query_access() override { return { .can_read = true, .can_write = false, }; } /** * 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 { if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return; this->backing_fs->read_file(this->archive_path, [&](coral::file_reader & archive_reader) { archive_file file{&archive_reader}; 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. } private: coral::fs * backing_fs; coral::path archive_path; }; }