/** * Bounds-checked little-/big-endian byte readers and writers. * * Red Alert 3's asset formats are little-endian, with one notable exception: * the `BIG4` index stores its counts, offsets and sizes big-endian. Both byte * orders are provided here. Every read is bounds-checked and throws * `format_error` rather than reading past the end of the buffer. */ export module ra3.assets:bytes; import std; import :error; export namespace ra3::assets { /** A cursor over a read-only byte range with checked accessors. */ class byte_reader { public: explicit byte_reader(std::span data) : data_(data) { } [[nodiscard]] auto size() const -> std::size_t { return this->data_.size(); } [[nodiscard]] auto position() const -> std::size_t { return this->pos_; } [[nodiscard]] auto remaining() const -> std::size_t { return this->data_.size() - this->pos_; } [[nodiscard]] auto empty() const -> bool { return this->pos_ >= this->data_.size(); } auto seek(std::size_t at) -> void { if (at > this->data_.size()) throw format_error("seek past end of buffer"); this->pos_ = at; } auto skip(std::size_t count) -> void { this->require(count); this->pos_ += count; } [[nodiscard]] auto read_u8() -> std::uint8_t { this->require(1); return std::to_integer(this->data_[this->pos_++]); } [[nodiscard]] auto read_bool() -> bool { return this->read_u8() != 0U; } [[nodiscard]] auto read_u16() -> std::uint16_t { return static_cast(this->read_le(2)); } [[nodiscard]] auto read_u24() -> std::uint32_t { return this->read_le(3); } [[nodiscard]] auto read_u32() -> std::uint32_t { return this->read_le(4); } [[nodiscard]] auto read_u64() -> std::uint64_t { return this->read_le(8); } [[nodiscard]] auto read_i16() -> std::int16_t { return static_cast(this->read_u16()); } [[nodiscard]] auto read_i32() -> std::int32_t { return static_cast(this->read_u32()); } [[nodiscard]] auto read_f32() -> float { return std::bit_cast(this->read_u32()); } [[nodiscard]] auto read_be_u16() -> std::uint16_t { return static_cast(this->read_be(2)); } [[nodiscard]] auto read_be_u32() -> std::uint32_t { return this->read_be(4); } /** A view of the next `count` bytes; the cursor advances past them. */ [[nodiscard]] auto read_bytes(std::size_t count) -> std::span { this->require(count); const auto view = this->data_.subspan(this->pos_, count); this->pos_ += count; return view; } /** `count` bytes decoded as Latin-1 (one byte per character). */ [[nodiscard]] auto read_ascii(std::size_t count) -> std::string { const auto view = this->read_bytes(count); return std::string{reinterpret_cast(view.data()), view.size()}; } /** A `u16`-length-prefixed ASCII string. */ [[nodiscard]] auto read_u16_prefixed_ascii() -> std::string { return this->read_ascii(this->read_u16()); } /** A `u16`-length-prefixed string, decoded as UTF-8 (ASCII-compatible). */ [[nodiscard]] auto read_u16_prefixed_ascii_as_utf8() -> std::string { return this->read_u16_prefixed_ascii(); } /** A `u16`-length-prefixed UTF-16LE string (length counted in code units). */ [[nodiscard]] auto read_u16_prefixed_utf16() -> std::u16string { const auto count = this->read_u16(); this->require(static_cast(count) * 2U); std::u16string text(count, u'\0'); for (std::uint16_t i = 0; i < count; ++i) text[i] = static_cast(this->read_u16()); return text; } /** A NUL-terminated ASCII string; the cursor stops just past the NUL. */ [[nodiscard]] auto read_cstring() -> std::string { std::string text; while (this->pos_ < this->data_.size()) { const auto ch = static_cast(this->read_u8()); if (ch == '\0') break; text.push_back(ch); } return text; } private: auto require(std::size_t count) const -> void { if (this->pos_ + count > this->data_.size()) throw format_error("unexpected end of buffer"); } [[nodiscard]] auto read_le(std::size_t width) -> std::uint64_t { this->require(width); std::uint64_t value = 0; for (std::size_t i = 0; i < width; ++i) value |= static_cast(std::to_integer(this->data_[this->pos_ + i])) << (8U * i); this->pos_ += width; return value; } [[nodiscard]] auto read_be(std::size_t width) -> std::uint64_t { this->require(width); std::uint64_t value = 0; for (std::size_t i = 0; i < width; ++i) value = (value << 8U) | static_cast(std::to_integer(this->data_[this->pos_ + i])); this->pos_ += width; return value; } std::span data_; std::size_t pos_ = 0; }; /** An append-only little-endian byte buffer. */ class byte_writer { public: [[nodiscard]] auto size() const -> std::size_t { return this->buffer_.size(); } [[nodiscard]] auto data() const -> std::span { return this->buffer_; } [[nodiscard]] auto take() -> std::vector { return std::move(this->buffer_); } auto write_u8(std::uint8_t value) -> void { this->buffer_.push_back(static_cast(value)); } auto write_bool(bool value) -> void { this->write_u8(value ? 1U : 0U); } auto write_u16(std::uint16_t value) -> void { this->write_le(value, 2); } auto write_u24(std::uint32_t value) -> void { this->write_le(value, 3); } auto write_u32(std::uint32_t value) -> void { this->write_le(value, 4); } auto write_u64(std::uint64_t value) -> void { this->write_le(value, 8); } auto write_i16(std::int16_t value) -> void { this->write_u16(static_cast(value)); } auto write_i32(std::int32_t value) -> void { this->write_u32(static_cast(value)); } auto write_f32(float value) -> void { this->write_u32(std::bit_cast(value)); } auto write_bytes(std::span bytes) -> void { this->buffer_.insert(this->buffer_.end(), bytes.begin(), bytes.end()); } auto write_ascii(std::string_view text) -> void { for (const auto ch: text) this->buffer_.push_back(static_cast(static_cast(ch))); } auto write_u16_prefixed_ascii(std::string_view text) -> void { this->write_u16(static_cast(text.size())); this->write_ascii(text); } auto write_u16_prefixed_utf16(std::u16string_view text) -> void { this->write_u16(static_cast(text.size())); for (const auto unit: text) { this->write_u16(static_cast(unit)); } } /** Overwrite a previously written `u32` (used to backpatch sizes). */ auto patch_u32(std::size_t offset, std::uint32_t value) -> void { if (offset + 4U > this->buffer_.size()) throw format_error("patch offset past end of buffer"); for (std::size_t i = 0; i < 4U; ++i) this->buffer_[offset + i] = static_cast((value >> (8U * i)) & 0xFFU); } private: auto write_le(std::uint64_t value, std::size_t width) -> void { for (std::size_t i = 0; i < width; ++i) this->buffer_.push_back(static_cast((value >> (8U * i)) & 0xFFU)); } std::vector buffer_; }; /** Convenience: encode a UTF-8 string (ASCII subset) as UTF-16LE code units. */ [[nodiscard]] inline auto to_utf16(std::string_view text) -> std::u16string { std::u16string out; out.reserve(text.size()); for (const auto ch: text) out.push_back(static_cast(static_cast(ch))); return out; } /** Convenience: narrow a UTF-16 string that is known to be within Latin-1. */ [[nodiscard]] inline auto to_ascii(std::u16string_view text) -> std::string { std::string out; out.reserve(text.size()); for (const auto unit: text) out.push_back(static_cast(unit & 0xFFU)); return out; } /** Read a whole file as bytes. */ [[nodiscard]] inline auto read_file(const std::filesystem::path &path) -> std::vector { std::ifstream in(path, std::ios::binary); if (!in) throw format_error("cannot open file: " + path.string()); in.seekg(0, std::ios::end); const auto end = in.tellg(); if (end < 0) throw format_error("cannot size file: " + path.string()); std::vector bytes(static_cast(end)); in.seekg(0, std::ios::beg); if (!bytes.empty()) in.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); return bytes; } /** Write bytes to a file, creating parent directories as needed. */ inline auto write_file(const std::filesystem::path &path, std::span bytes) -> void { std::error_code ec; if (!path.parent_path().empty()) std::filesystem::create_directories(path.parent_path(), ec); std::ofstream out(path, std::ios::binary); if (!out) throw format_error("cannot write file: " + path.string()); out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); if (!out) throw format_error("cannot write file: " + path.string()); } } // namespace ra3::assets