Refine and fix bugs in the Oar library
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
2a0f821e64
commit
78ae403f61
337
source/oar.cpp
337
source/oar.cpp
|
@ -3,14 +3,25 @@ export module oar;
|
|||
import coral;
|
||||
import coral.files;
|
||||
|
||||
constexpr coral::usize signature_length{4};
|
||||
/**
|
||||
* Length of the full magic signature at the beginning of an Oar file.
|
||||
*/
|
||||
constexpr coral::usize signature_length {4};
|
||||
|
||||
constexpr coral::usize signature_version_length{1};
|
||||
/**
|
||||
* 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 coral::usize signature_identifier_length{signature_length - signature_version_length};
|
||||
|
||||
constexpr coral::u8 signature_magic[signature_length]{'o', 'a', 'r', 0};
|
||||
/**
|
||||
* 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];
|
||||
|
||||
|
@ -21,6 +32,9 @@ struct header {
|
|||
|
||||
static_assert(sizeof(header) == 512);
|
||||
|
||||
/**
|
||||
* Oar file header format.
|
||||
*/
|
||||
struct entry {
|
||||
coral::path path;
|
||||
|
||||
|
@ -33,144 +47,168 @@ struct entry {
|
|||
|
||||
static_assert(sizeof(entry) == 512);
|
||||
|
||||
export namespace oar {
|
||||
struct archive_file_reader : public coral::file_reader {
|
||||
enum class [[nodiscard]] close_result {
|
||||
ok,
|
||||
};
|
||||
|
||||
enum class [[nodiscard]] open_result {
|
||||
ok,
|
||||
io_unavailable,
|
||||
archive_invalid,
|
||||
archive_unsupported,
|
||||
not_found,
|
||||
};
|
||||
|
||||
archive_file_reader(coral::file_reader * archive_reader) {
|
||||
this->archive_reader = archive_reader;
|
||||
this->data_offset = 0;
|
||||
this->data_length = 0;
|
||||
this->data_cursor = 0;
|
||||
}
|
||||
|
||||
close_result close() {
|
||||
return close_result::ok;
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
return this->data_offset >= sizeof(header);
|
||||
}
|
||||
|
||||
open_result open(coral::path const & file_path) {
|
||||
if (this->is_open()) switch (this->close()) {
|
||||
case close_result::ok: break;
|
||||
default: coral::unreachable();
|
||||
}
|
||||
|
||||
if (!this->archive_reader->seek(0).is_ok()) return open_result::io_unavailable;
|
||||
|
||||
constexpr coral::usize header_size = sizeof(header);
|
||||
coral::u8 archive_header_buffer[header_size]{0};
|
||||
|
||||
{
|
||||
coral::expected const read_bytes{archive_reader->read(archive_header_buffer)};
|
||||
|
||||
if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size))
|
||||
return open_result::archive_invalid;
|
||||
}
|
||||
|
||||
header const * const archive_header{reinterpret_cast<header const *>(archive_header_buffer)};
|
||||
|
||||
if (!coral::equals({archive_header->signature_magic, signature_identifier_length},
|
||||
{signature_magic, signature_identifier_length})) return open_result::archive_invalid;
|
||||
|
||||
if (archive_header->signature_magic[signature_identifier_length] !=
|
||||
signature_magic[signature_identifier_length]) return open_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 file_entry_buffer[entry_size]{0};
|
||||
|
||||
while (head <= tail) {
|
||||
coral::u64 const midpoint{head + ((tail - head) / 2)};
|
||||
|
||||
if (!archive_reader->seek(header_size + (entry_size * midpoint)).is_ok())
|
||||
return open_result::archive_invalid;
|
||||
|
||||
{
|
||||
coral::expected const read_bytes{archive_reader->read(file_entry_buffer)};
|
||||
|
||||
if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size))
|
||||
return open_result::archive_invalid;
|
||||
}
|
||||
|
||||
entry const * const archive_entry{reinterpret_cast<entry const *>(file_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 open_result::ok;
|
||||
}
|
||||
|
||||
if (comparison > 0) {
|
||||
head = (midpoint + 1);
|
||||
} else {
|
||||
tail = (midpoint - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return open_result::not_found;
|
||||
}
|
||||
|
||||
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & buffer) override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
if (this->archive_reader->seek(this->data_offset + this->data_cursor).is_ok())
|
||||
return coral::io_error::unavailable;
|
||||
|
||||
coral::expected const bytes_read{this->archive_reader->read(buffer.sliced(0,
|
||||
coral::min(buffer.length, static_cast<coral::usize>((
|
||||
this->data_offset + this->data_length) - this->data_cursor))))};
|
||||
|
||||
if (!bytes_read.is_ok()) this->data_cursor += bytes_read.value();
|
||||
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
return coral::io_error::unavailable;
|
||||
}
|
||||
|
||||
coral::expected<coral::u64, coral::io_error> tell() override {
|
||||
if (!this->is_open()) return coral::io_error::unavailable;
|
||||
|
||||
return this->data_cursor;
|
||||
}
|
||||
|
||||
private:
|
||||
coral::file_reader * archive_reader;
|
||||
|
||||
coral::u64 data_offset;
|
||||
|
||||
coral::u64 data_length;
|
||||
|
||||
coral::u64 data_cursor;
|
||||
/**
|
||||
* 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<header const *>(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<entry const *>(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<coral::usize, coral::io_error> read(coral::slice<coral::u8> 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<coral::u64, coral::io_error> 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<coral::u64, coral::io_error> 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,
|
||||
|
@ -178,27 +216,34 @@ 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.
|
||||
*/
|
||||
void read_file(coral::path const & file_path,
|
||||
coral::closure<void(coral::file_reader &)> 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, [&](coral::file_reader & archive_reader) {
|
||||
archive_file_reader file_reader{&archive_reader};
|
||||
this->backing_fs->read_file(this->archive_path,
|
||||
[&](coral::file_reader & archive_reader) {
|
||||
archive_file file{&archive_reader};
|
||||
|
||||
if (file_reader.open(file_path) != archive_file_reader::open_result::ok) return;
|
||||
if (file.find(file_path) != archive_file::find_result::ok) return;
|
||||
|
||||
then(file_reader);
|
||||
|
||||
if (file_reader.close() != archive_file_reader::close_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<void(coral::file_writer &)> const & then) override {
|
||||
|
||||
// Read-only file system.
|
||||
}
|
||||
// Read-only file system.
|
||||
}
|
||||
|
||||
private:
|
||||
coral::fs * backing_fs;
|
||||
|
|
Loading…
Reference in New Issue