aboutsummaryrefslogtreecommitdiff
path: root/crates/rsli/src
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/rsli/src
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/rsli/src')
-rw-r--r--crates/rsli/src/compress/deflate.rs14
-rw-r--r--crates/rsli/src/compress/lzh.rs303
-rw-r--r--crates/rsli/src/compress/lzss.rs79
-rw-r--r--crates/rsli/src/compress/mod.rs9
-rw-r--r--crates/rsli/src/compress/xor.rs29
-rw-r--r--crates/rsli/src/error.rs140
-rw-r--r--crates/rsli/src/lib.rs470
-rw-r--r--crates/rsli/src/parse.rs278
-rw-r--r--crates/rsli/src/tests.rs1338
9 files changed, 0 insertions, 2660 deletions
diff --git a/crates/rsli/src/compress/deflate.rs b/crates/rsli/src/compress/deflate.rs
deleted file mode 100644
index 6b8ea73..0000000
--- a/crates/rsli/src/compress/deflate.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use crate::error::Error;
-use crate::Result;
-use flate2::read::DeflateDecoder;
-use std::io::Read;
-
-/// Decode raw Deflate (RFC 1951) payload.
-pub fn decode_deflate(packed: &[u8]) -> Result<Vec<u8>> {
- let mut out = Vec::new();
- let mut decoder = DeflateDecoder::new(packed);
- decoder
- .read_to_end(&mut out)
- .map_err(|_| Error::DecompressionFailed("deflate"))?;
- Ok(out)
-}
diff --git a/crates/rsli/src/compress/lzh.rs b/crates/rsli/src/compress/lzh.rs
deleted file mode 100644
index 9486c50..0000000
--- a/crates/rsli/src/compress/lzh.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-use super::xor::XorState;
-use crate::error::Error;
-use crate::Result;
-
-pub(crate) const LZH_N: usize = 4096;
-pub(crate) const LZH_F: usize = 60;
-pub(crate) const LZH_THRESHOLD: usize = 2;
-pub(crate) const LZH_N_CHAR: usize = 256 - LZH_THRESHOLD + LZH_F;
-pub(crate) const LZH_T: usize = LZH_N_CHAR * 2 - 1;
-pub(crate) const LZH_R: usize = LZH_T - 1;
-pub(crate) const LZH_MAX_FREQ: u16 = 0x8000;
-
-/// LZSS-Huffman decompression with optional on-the-fly XOR decryption.
-pub fn lzss_huffman_decompress(
- data: &[u8],
- expected_size: usize,
- xor_key: Option<u16>,
-) -> Result<Vec<u8>> {
- let mut decoder = LzhDecoder::new(data, xor_key);
- decoder.decode(expected_size)
-}
-
-struct LzhDecoder<'a> {
- bit_reader: BitReader<'a>,
- text: [u8; LZH_N],
- freq: [u16; LZH_T + 1],
- parent: [usize; LZH_T + LZH_N_CHAR],
- son: [usize; LZH_T],
- d_code: [u8; 256],
- d_len: [u8; 256],
- ring_pos: usize,
-}
-
-impl<'a> LzhDecoder<'a> {
- fn new(data: &'a [u8], xor_key: Option<u16>) -> Self {
- let mut decoder = Self {
- bit_reader: BitReader::new(data, xor_key),
- text: [0x20u8; LZH_N],
- freq: [0u16; LZH_T + 1],
- parent: [0usize; LZH_T + LZH_N_CHAR],
- son: [0usize; LZH_T],
- d_code: [0u8; 256],
- d_len: [0u8; 256],
- ring_pos: LZH_N - LZH_F,
- };
- decoder.init_tables();
- decoder.start_huff();
- decoder
- }
-
- fn decode(&mut self, expected_size: usize) -> Result<Vec<u8>> {
- let mut out = Vec::with_capacity(expected_size);
-
- while out.len() < expected_size {
- let c = self.decode_char()?;
- if c < 256 {
- let byte = c as u8;
- out.push(byte);
- self.text[self.ring_pos] = byte;
- self.ring_pos = (self.ring_pos + 1) & (LZH_N - 1);
- } else {
- let mut offset = self.decode_position()?;
- offset = (self.ring_pos.wrapping_sub(offset).wrapping_sub(1)) & (LZH_N - 1);
- let mut length = c.saturating_sub(253);
-
- while length > 0 && out.len() < expected_size {
- let byte = self.text[offset];
- out.push(byte);
- self.text[self.ring_pos] = byte;
- self.ring_pos = (self.ring_pos + 1) & (LZH_N - 1);
- offset = (offset + 1) & (LZH_N - 1);
- length -= 1;
- }
- }
- }
-
- if out.len() != expected_size {
- return Err(Error::DecompressionFailed("lzss-huffman"));
- }
- Ok(out)
- }
-
- fn init_tables(&mut self) {
- let d_code_group_counts = [1usize, 3, 8, 12, 24, 16];
- let d_len_group_counts = [32usize, 48, 64, 48, 48, 16];
-
- let mut group_index = 0u8;
- let mut idx = 0usize;
- let mut run = 32usize;
- for count in d_code_group_counts {
- for _ in 0..count {
- for _ in 0..run {
- self.d_code[idx] = group_index;
- idx += 1;
- }
- group_index = group_index.wrapping_add(1);
- }
- run >>= 1;
- }
-
- let mut len = 3u8;
- idx = 0;
- for count in d_len_group_counts {
- for _ in 0..count {
- self.d_len[idx] = len;
- idx += 1;
- }
- len = len.saturating_add(1);
- }
- }
-
- fn start_huff(&mut self) {
- for i in 0..LZH_N_CHAR {
- self.freq[i] = 1;
- self.son[i] = i + LZH_T;
- self.parent[i + LZH_T] = i;
- }
-
- let mut i = 0usize;
- let mut j = LZH_N_CHAR;
- while j <= LZH_R {
- self.freq[j] = self.freq[i].saturating_add(self.freq[i + 1]);
- self.son[j] = i;
- self.parent[i] = j;
- self.parent[i + 1] = j;
- i += 2;
- j += 1;
- }
-
- self.freq[LZH_T] = u16::MAX;
- self.parent[LZH_R] = 0;
- }
-
- fn decode_char(&mut self) -> Result<usize> {
- let mut node = self.son[LZH_R];
- while node < LZH_T {
- let bit = usize::from(self.bit_reader.read_bit()?);
- let branch = node
- .checked_add(bit)
- .ok_or(Error::DecompressionFailed("lzss-huffman tree overflow"))?;
- node = *self.son.get(branch).ok_or(Error::DecompressionFailed(
- "lzss-huffman tree out of bounds",
- ))?;
- }
-
- let c = node - LZH_T;
- self.update(c);
- Ok(c)
- }
-
- fn decode_position(&mut self) -> Result<usize> {
- let i = self.bit_reader.read_bits(8)? as usize;
- let mut c = usize::from(self.d_code[i]) << 6;
- let mut j = usize::from(self.d_len[i]).saturating_sub(2);
-
- while j > 0 {
- j -= 1;
- c |= usize::from(self.bit_reader.read_bit()?) << j;
- }
-
- Ok(c | (i & 0x3F))
- }
-
- fn update(&mut self, c: usize) {
- if self.freq[LZH_R] == LZH_MAX_FREQ {
- self.reconstruct();
- }
-
- let mut current = self.parent[c + LZH_T];
- loop {
- self.freq[current] = self.freq[current].saturating_add(1);
- let freq = self.freq[current];
-
- if current + 1 < self.freq.len() && freq > self.freq[current + 1] {
- let mut swap_idx = current + 1;
- while swap_idx + 1 < self.freq.len() && freq > self.freq[swap_idx + 1] {
- swap_idx += 1;
- }
-
- self.freq.swap(current, swap_idx);
-
- let left = self.son[current];
- let right = self.son[swap_idx];
- self.son[current] = right;
- self.son[swap_idx] = left;
-
- self.parent[left] = swap_idx;
- if left < LZH_T {
- self.parent[left + 1] = swap_idx;
- }
-
- self.parent[right] = current;
- if right < LZH_T {
- self.parent[right + 1] = current;
- }
-
- current = swap_idx;
- }
-
- current = self.parent[current];
- if current == 0 {
- break;
- }
- }
- }
-
- fn reconstruct(&mut self) {
- let mut j = 0usize;
- for i in 0..LZH_T {
- if self.son[i] >= LZH_T {
- self.freq[j] = (self.freq[i].saturating_add(1)) / 2;
- self.son[j] = self.son[i];
- j += 1;
- }
- }
-
- let mut i = 0usize;
- let mut current = LZH_N_CHAR;
- while current < LZH_T {
- let sum = self.freq[i].saturating_add(self.freq[i + 1]);
- self.freq[current] = sum;
-
- let mut insert_at = current;
- while insert_at > 0 && sum < self.freq[insert_at - 1] {
- insert_at -= 1;
- }
-
- for move_idx in (insert_at..current).rev() {
- self.freq[move_idx + 1] = self.freq[move_idx];
- self.son[move_idx + 1] = self.son[move_idx];
- }
-
- self.freq[insert_at] = sum;
- self.son[insert_at] = i;
-
- i += 2;
- current += 1;
- }
-
- for idx in 0..LZH_T {
- let node = self.son[idx];
- self.parent[node] = idx;
- if node < LZH_T {
- self.parent[node + 1] = idx;
- }
- }
-
- self.freq[LZH_T] = u16::MAX;
- self.parent[LZH_R] = 0;
- }
-}
-
-struct BitReader<'a> {
- data: &'a [u8],
- byte_pos: usize,
- bit_mask: u8,
- current_byte: u8,
- xor_state: Option<XorState>,
-}
-
-impl<'a> BitReader<'a> {
- fn new(data: &'a [u8], xor_key: Option<u16>) -> Self {
- Self {
- data,
- byte_pos: 0,
- bit_mask: 0x80,
- current_byte: 0,
- xor_state: xor_key.map(XorState::new),
- }
- }
-
- fn read_bit(&mut self) -> Result<u8> {
- if self.bit_mask == 0x80 {
- let Some(mut byte) = self.data.get(self.byte_pos).copied() else {
- return Err(Error::DecompressionFailed("lzss-huffman: unexpected EOF"));
- };
- if let Some(state) = &mut self.xor_state {
- byte = state.decrypt_byte(byte);
- }
- self.current_byte = byte;
- }
-
- let bit = if (self.current_byte & self.bit_mask) != 0 {
- 1
- } else {
- 0
- };
- self.bit_mask >>= 1;
- if self.bit_mask == 0 {
- self.bit_mask = 0x80;
- self.byte_pos = self.byte_pos.saturating_add(1);
- }
- Ok(bit)
- }
-
- fn read_bits(&mut self, bits: usize) -> Result<u32> {
- let mut value = 0u32;
- for _ in 0..bits {
- value = (value << 1) | u32::from(self.read_bit()?);
- }
- Ok(value)
- }
-}
diff --git a/crates/rsli/src/compress/lzss.rs b/crates/rsli/src/compress/lzss.rs
deleted file mode 100644
index d30345c..0000000
--- a/crates/rsli/src/compress/lzss.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use super::xor::XorState;
-use crate::error::Error;
-use crate::Result;
-
-/// Simple LZSS decompression with optional on-the-fly XOR decryption
-pub fn lzss_decompress_simple(
- data: &[u8],
- expected_size: usize,
- xor_key: Option<u16>,
-) -> Result<Vec<u8>> {
- let mut ring = [0x20u8; 0x1000];
- let mut ring_pos = 0xFEEusize;
- let mut out = Vec::with_capacity(expected_size);
- let mut in_pos = 0usize;
-
- let mut control = 0u8;
- let mut bits_left = 0u8;
-
- // XOR state for on-the-fly decryption
- let mut xor_state = xor_key.map(XorState::new);
-
- // Helper to read byte with optional XOR decryption
- let read_byte = |pos: usize, state: &mut Option<XorState>| -> Option<u8> {
- let encrypted = data.get(pos).copied()?;
- Some(if let Some(ref mut s) = state {
- s.decrypt_byte(encrypted)
- } else {
- encrypted
- })
- };
-
- while out.len() < expected_size {
- if bits_left == 0 {
- let byte = read_byte(in_pos, &mut xor_state)
- .ok_or(Error::DecompressionFailed("lzss-simple: unexpected EOF"))?;
- control = byte;
- in_pos += 1;
- bits_left = 8;
- }
-
- if (control & 1) != 0 {
- let byte = read_byte(in_pos, &mut xor_state)
- .ok_or(Error::DecompressionFailed("lzss-simple: unexpected EOF"))?;
- in_pos += 1;
-
- out.push(byte);
- ring[ring_pos] = byte;
- ring_pos = (ring_pos + 1) & 0x0FFF;
- } else {
- let low = read_byte(in_pos, &mut xor_state)
- .ok_or(Error::DecompressionFailed("lzss-simple: unexpected EOF"))?;
- let high = read_byte(in_pos + 1, &mut xor_state)
- .ok_or(Error::DecompressionFailed("lzss-simple: unexpected EOF"))?;
- in_pos += 2;
-
- let offset = usize::from(low) | (usize::from(high & 0xF0) << 4);
- let length = usize::from((high & 0x0F) + 3);
-
- for step in 0..length {
- let byte = ring[(offset + step) & 0x0FFF];
- out.push(byte);
- ring[ring_pos] = byte;
- ring_pos = (ring_pos + 1) & 0x0FFF;
- if out.len() >= expected_size {
- break;
- }
- }
- }
-
- control >>= 1;
- bits_left -= 1;
- }
-
- if out.len() != expected_size {
- return Err(Error::DecompressionFailed("lzss-simple"));
- }
-
- Ok(out)
-}
diff --git a/crates/rsli/src/compress/mod.rs b/crates/rsli/src/compress/mod.rs
deleted file mode 100644
index bd23143..0000000
--- a/crates/rsli/src/compress/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-pub mod deflate;
-pub mod lzh;
-pub mod lzss;
-pub mod xor;
-
-pub use deflate::decode_deflate;
-pub use lzh::lzss_huffman_decompress;
-pub use lzss::lzss_decompress_simple;
-pub use xor::{xor_stream, XorState};
diff --git a/crates/rsli/src/compress/xor.rs b/crates/rsli/src/compress/xor.rs
deleted file mode 100644
index c4c3d7d..0000000
--- a/crates/rsli/src/compress/xor.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-/// XOR cipher state for RsLi format
-pub struct XorState {
- lo: u8,
- hi: u8,
-}
-
-impl XorState {
- /// Create new XOR state from 16-bit key
- pub fn new(key16: u16) -> Self {
- Self {
- lo: (key16 & 0xFF) as u8,
- hi: ((key16 >> 8) & 0xFF) as u8,
- }
- }
-
- /// Decrypt a single byte and update state
- pub fn decrypt_byte(&mut self, encrypted: u8) -> u8 {
- self.lo = self.hi ^ self.lo.wrapping_shl(1);
- let decrypted = encrypted ^ self.lo;
- self.hi = self.lo ^ (self.hi >> 1);
- decrypted
- }
-}
-
-/// Decrypt entire buffer with XOR stream cipher
-pub fn xor_stream(data: &[u8], key16: u16) -> Vec<u8> {
- let mut state = XorState::new(key16);
- data.iter().map(|&b| state.decrypt_byte(b)).collect()
-}
diff --git a/crates/rsli/src/error.rs b/crates/rsli/src/error.rs
deleted file mode 100644
index 5a36101..0000000
--- a/crates/rsli/src/error.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-use core::fmt;
-
-#[derive(Debug)]
-#[non_exhaustive]
-pub enum Error {
- Io(std::io::Error),
-
- InvalidMagic {
- got: [u8; 2],
- },
- UnsupportedVersion {
- got: u8,
- },
- InvalidEntryCount {
- got: i16,
- },
- TooManyEntries {
- got: usize,
- },
-
- EntryTableOutOfBounds {
- table_offset: u64,
- table_len: u64,
- file_len: u64,
- },
- EntryTableDecryptFailed,
- CorruptEntryTable(&'static str),
-
- EntryIdOutOfRange {
- id: u32,
- entry_count: u32,
- },
- EntryDataOutOfBounds {
- id: u32,
- offset: u64,
- size: u32,
- file_len: u64,
- },
-
- AoTrailerInvalid,
- MediaOverlayOutOfBounds {
- overlay: u32,
- file_len: u64,
- },
-
- UnsupportedMethod {
- raw: u32,
- },
- PackedSizePastEof {
- id: u32,
- offset: u64,
- packed_size: u32,
- file_len: u64,
- },
- DeflateEofPlusOneQuirkRejected {
- id: u32,
- },
-
- DecompressionFailed(&'static str),
- OutputSizeMismatch {
- expected: u32,
- got: u32,
- },
-
- IntegerOverflow,
-}
-
-impl From<std::io::Error> for Error {
- fn from(value: std::io::Error) -> Self {
- Self::Io(value)
- }
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Error::Io(e) => write!(f, "I/O error: {e}"),
- Error::InvalidMagic { got } => write!(f, "invalid RsLi magic: {got:02X?}"),
- Error::UnsupportedVersion { got } => write!(f, "unsupported RsLi version: {got:#x}"),
- Error::InvalidEntryCount { got } => write!(f, "invalid entry_count: {got}"),
- Error::TooManyEntries { got } => write!(f, "too many entries: {got} exceeds u32::MAX"),
- Error::EntryTableOutOfBounds {
- table_offset,
- table_len,
- file_len,
- } => write!(
- f,
- "entry table out of bounds: off={table_offset}, len={table_len}, file={file_len}"
- ),
- Error::EntryTableDecryptFailed => write!(f, "failed to decrypt entry table"),
- Error::CorruptEntryTable(s) => write!(f, "corrupt entry table: {s}"),
- Error::EntryIdOutOfRange { id, entry_count } => {
- write!(f, "entry id out of range: id={id}, count={entry_count}")
- }
- Error::EntryDataOutOfBounds {
- id,
- offset,
- size,
- file_len,
- } => write!(
- f,
- "entry data out of bounds: id={id}, off={offset}, size={size}, file={file_len}"
- ),
- Error::AoTrailerInvalid => write!(f, "invalid AO trailer"),
- Error::MediaOverlayOutOfBounds { overlay, file_len } => {
- write!(
- f,
- "media overlay out of bounds: overlay={overlay}, file={file_len}"
- )
- }
- Error::UnsupportedMethod { raw } => write!(f, "unsupported packing method: {raw:#x}"),
- Error::PackedSizePastEof {
- id,
- offset,
- packed_size,
- file_len,
- } => write!(
- f,
- "packed range past EOF: id={id}, off={offset}, size={packed_size}, file={file_len}"
- ),
- Error::DeflateEofPlusOneQuirkRejected { id } => {
- write!(f, "deflate EOF+1 quirk rejected for entry {id}")
- }
- Error::DecompressionFailed(s) => write!(f, "decompression failed: {s}"),
- Error::OutputSizeMismatch { expected, got } => {
- write!(f, "output size mismatch: expected={expected}, got={got}")
- }
- Error::IntegerOverflow => write!(f, "integer overflow"),
- }
- }
-}
-
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Io(err) => Some(err),
- _ => None,
- }
- }
-}
diff --git a/crates/rsli/src/lib.rs b/crates/rsli/src/lib.rs
deleted file mode 100644
index 1ce3b1f..0000000
--- a/crates/rsli/src/lib.rs
+++ /dev/null
@@ -1,470 +0,0 @@
-pub mod compress;
-pub mod error;
-pub mod parse;
-
-use crate::compress::{
- decode_deflate, lzss_decompress_simple, lzss_huffman_decompress, xor_stream,
-};
-use crate::error::Error;
-use crate::parse::{c_name_bytes, cmp_c_string, parse_library};
-use common::{OutputBuffer, ResourceData};
-use std::cmp::Ordering;
-use std::fs;
-use std::path::Path;
-use std::sync::Arc;
-
-pub type Result<T> = core::result::Result<T, Error>;
-
-#[derive(Clone, Debug)]
-pub struct OpenOptions {
- pub allow_ao_trailer: bool,
- pub allow_deflate_eof_plus_one: bool,
-}
-
-impl Default for OpenOptions {
- fn default() -> Self {
- Self {
- allow_ao_trailer: true,
- allow_deflate_eof_plus_one: true,
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct LibraryHeader {
- pub raw: [u8; 32],
- pub magic: [u8; 2],
- pub reserved: u8,
- pub version: u8,
- pub entry_count: i16,
- pub presorted_flag: u16,
- pub xor_seed: u32,
-}
-
-#[derive(Clone, Debug)]
-pub struct AoTrailer {
- pub raw: [u8; 6],
- pub overlay: u32,
-}
-
-#[derive(Debug)]
-pub struct Library {
- bytes: Arc<[u8]>,
- entries: Vec<EntryRecord>,
- header: LibraryHeader,
- ao_trailer: Option<AoTrailer>,
- #[cfg(test)]
- pub(crate) table_plain_original: Vec<u8>,
- #[cfg(test)]
- pub(crate) source_size: usize,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub struct EntryId(pub u32);
-
-#[derive(Clone, Debug)]
-pub struct EntryMeta {
- pub name: String,
- pub flags: i32,
- pub method: PackMethod,
- pub data_offset: u64,
- pub packed_size: u32,
- pub unpacked_size: u32,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum PackMethod {
- None,
- XorOnly,
- Lzss,
- XorLzss,
- LzssHuffman,
- XorLzssHuffman,
- Deflate,
- Unknown(u32),
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct EntryRef<'a> {
- pub id: EntryId,
- pub meta: &'a EntryMeta,
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct EntryInspect<'a> {
- pub id: EntryId,
- pub meta: &'a EntryMeta,
- pub name_raw: &'a [u8; 12],
- pub service_tail: &'a [u8; 4],
- pub sort_to_original: i16,
- pub data_offset_raw: u32,
-}
-
-pub struct PackedResource {
- pub meta: EntryMeta,
- pub packed: Vec<u8>,
-}
-
-#[derive(Clone, Debug)]
-pub(crate) struct EntryRecord {
- pub(crate) meta: EntryMeta,
- pub(crate) name_raw: [u8; 12],
- pub(crate) service_tail: [u8; 4],
- pub(crate) sort_to_original: i16,
- pub(crate) key16: u16,
- pub(crate) data_offset_raw: u32,
- pub(crate) packed_size_declared: u32,
- pub(crate) packed_size_available: usize,
- pub(crate) effective_offset: usize,
-}
-
-impl Library {
- pub fn open_path(path: impl AsRef<Path>) -> Result<Self> {
- Self::open_path_with(path, OpenOptions::default())
- }
-
- pub fn open_path_with(path: impl AsRef<Path>, opts: OpenOptions) -> Result<Self> {
- let bytes = fs::read(path.as_ref())?;
- let arc: Arc<[u8]> = Arc::from(bytes.into_boxed_slice());
- parse_library(arc, opts)
- }
-
- pub fn header(&self) -> &LibraryHeader {
- &self.header
- }
-
- pub fn ao_trailer(&self) -> Option<&AoTrailer> {
- self.ao_trailer.as_ref()
- }
-
- pub fn entry_count(&self) -> usize {
- self.entries.len()
- }
-
- pub fn entries(&self) -> impl Iterator<Item = EntryRef<'_>> {
- self.entries.iter().enumerate().filter_map(|(idx, entry)| {
- let id = u32::try_from(idx).ok()?;
- Some(EntryRef {
- id: EntryId(id),
- meta: &entry.meta,
- })
- })
- }
-
- pub fn entries_inspect(&self) -> impl Iterator<Item = EntryInspect<'_>> {
- self.entries.iter().enumerate().filter_map(|(idx, entry)| {
- let id = u32::try_from(idx).ok()?;
- Some(EntryInspect {
- id: EntryId(id),
- meta: &entry.meta,
- name_raw: &entry.name_raw,
- service_tail: &entry.service_tail,
- sort_to_original: entry.sort_to_original,
- data_offset_raw: entry.data_offset_raw,
- })
- })
- }
-
- pub fn find(&self, name: &str) -> Option<EntryId> {
- if self.entries.is_empty() {
- return None;
- }
-
- const MAX_INLINE_NAME: usize = 12;
-
- // Fast path: use stack allocation for short ASCII names (95% of cases)
- if name.len() <= MAX_INLINE_NAME && name.is_ascii() {
- let mut buf = [0u8; MAX_INLINE_NAME];
- for (i, &b) in name.as_bytes().iter().enumerate() {
- buf[i] = b.to_ascii_uppercase();
- }
- return self.find_impl(&buf[..name.len()]);
- }
-
- // Slow path: heap allocation for long or non-ASCII names
- let query = name.to_ascii_uppercase();
- self.find_impl(query.as_bytes())
- }
-
- fn find_impl(&self, query_bytes: &[u8]) -> Option<EntryId> {
- // Binary search
- let mut low = 0usize;
- let mut high = self.entries.len();
- while low < high {
- let mid = low + (high - low) / 2;
- let idx = self.entries[mid].sort_to_original;
- if idx < 0 {
- break;
- }
- let idx = usize::try_from(idx).ok()?;
- if idx >= self.entries.len() {
- break;
- }
-
- let cmp = cmp_c_string(query_bytes, c_name_bytes(&self.entries[idx].name_raw));
- match cmp {
- Ordering::Less => high = mid,
- Ordering::Greater => low = mid + 1,
- Ordering::Equal => {
- let id = u32::try_from(idx).ok()?;
- return Some(EntryId(id));
- }
- }
- }
-
- // Linear fallback search
- self.entries.iter().enumerate().find_map(|(idx, entry)| {
- if cmp_c_string(query_bytes, c_name_bytes(&entry.name_raw)) == Ordering::Equal {
- let id = u32::try_from(idx).ok()?;
- Some(EntryId(id))
- } else {
- None
- }
- })
- }
-
- pub fn get(&self, id: EntryId) -> Option<EntryRef<'_>> {
- let idx = usize::try_from(id.0).ok()?;
- let entry = self.entries.get(idx)?;
- Some(EntryRef {
- id,
- meta: &entry.meta,
- })
- }
-
- pub fn inspect(&self, id: EntryId) -> Option<EntryInspect<'_>> {
- let idx = usize::try_from(id.0).ok()?;
- let entry = self.entries.get(idx)?;
- Some(EntryInspect {
- id,
- meta: &entry.meta,
- name_raw: &entry.name_raw,
- service_tail: &entry.service_tail,
- sort_to_original: entry.sort_to_original,
- data_offset_raw: entry.data_offset_raw,
- })
- }
-
- pub fn load(&self, id: EntryId) -> Result<Vec<u8>> {
- let entry = self.entry_by_id(id)?;
- let packed = self.packed_slice(id, entry)?;
- decode_payload(
- packed,
- entry.meta.method,
- entry.key16,
- entry.meta.unpacked_size,
- )
- }
-
- pub fn load_into(&self, id: EntryId, out: &mut dyn OutputBuffer) -> Result<usize> {
- let decoded = self.load(id)?;
- out.write_exact(&decoded)?;
- Ok(decoded.len())
- }
-
- pub fn load_packed(&self, id: EntryId) -> Result<PackedResource> {
- let entry = self.entry_by_id(id)?;
- let packed = self.packed_slice(id, entry)?.to_vec();
- Ok(PackedResource {
- meta: entry.meta.clone(),
- packed,
- })
- }
-
- pub fn unpack(&self, packed: &PackedResource) -> Result<Vec<u8>> {
- let key16 = self.resolve_key_for_meta(&packed.meta).unwrap_or(0);
-
- let method = packed.meta.method;
- if needs_xor_key(method) && self.resolve_key_for_meta(&packed.meta).is_none() {
- return Err(Error::CorruptEntryTable(
- "cannot resolve XOR key for packed resource",
- ));
- }
-
- decode_payload(&packed.packed, method, key16, packed.meta.unpacked_size)
- }
-
- pub fn load_fast(&self, id: EntryId) -> Result<ResourceData<'_>> {
- let entry = self.entry_by_id(id)?;
- if entry.meta.method == PackMethod::None {
- let packed = self.packed_slice(id, entry)?;
- let size =
- usize::try_from(entry.meta.unpacked_size).map_err(|_| Error::IntegerOverflow)?;
- if packed.len() < size {
- return Err(Error::OutputSizeMismatch {
- expected: entry.meta.unpacked_size,
- got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
- });
- }
- return Ok(ResourceData::Borrowed(&packed[..size]));
- }
- Ok(ResourceData::Owned(self.load(id)?))
- }
-
- fn entry_by_id(&self, id: EntryId) -> Result<&EntryRecord> {
- let idx = usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?;
- self.entries
- .get(idx)
- .ok_or_else(|| Error::EntryIdOutOfRange {
- id: id.0,
- entry_count: saturating_u32_len(self.entries.len()),
- })
- }
-
- fn packed_slice<'a>(&'a self, id: EntryId, entry: &EntryRecord) -> Result<&'a [u8]> {
- let start = entry.effective_offset;
- let end = start
- .checked_add(entry.packed_size_available)
- .ok_or(Error::IntegerOverflow)?;
- self.bytes
- .get(start..end)
- .ok_or(Error::EntryDataOutOfBounds {
- id: id.0,
- offset: u64::try_from(start).unwrap_or(u64::MAX),
- size: entry.packed_size_declared,
- file_len: u64::try_from(self.bytes.len()).unwrap_or(u64::MAX),
- })
- }
-
- fn resolve_key_for_meta(&self, meta: &EntryMeta) -> Option<u16> {
- self.entries
- .iter()
- .find(|entry| {
- entry.meta.name == meta.name
- && entry.meta.flags == meta.flags
- && entry.meta.data_offset == meta.data_offset
- && entry.meta.packed_size == meta.packed_size
- && entry.meta.unpacked_size == meta.unpacked_size
- && entry.meta.method == meta.method
- })
- .map(|entry| entry.key16)
- }
-
- #[cfg(test)]
- pub(crate) fn rebuild_from_parsed_metadata(&self) -> Result<Vec<u8>> {
- let trailer_len = usize::from(self.ao_trailer.is_some()) * 6;
- let pre_trailer_size = self
- .source_size
- .checked_sub(trailer_len)
- .ok_or(Error::IntegerOverflow)?;
-
- let count = self.entries.len();
- let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
- let table_end = 32usize
- .checked_add(table_len)
- .ok_or(Error::IntegerOverflow)?;
- if pre_trailer_size < table_end {
- return Err(Error::EntryTableOutOfBounds {
- table_offset: 32,
- table_len: u64::try_from(table_len).map_err(|_| Error::IntegerOverflow)?,
- file_len: u64::try_from(pre_trailer_size).map_err(|_| Error::IntegerOverflow)?,
- });
- }
-
- let mut out = vec![0u8; pre_trailer_size];
- out[0..32].copy_from_slice(&self.header.raw);
- let encrypted_table = xor_stream(
- &self.table_plain_original,
- (self.header.xor_seed & 0xFFFF) as u16,
- );
- out[32..table_end].copy_from_slice(&encrypted_table);
-
- let mut occupied = vec![false; pre_trailer_size];
- for byte in occupied.iter_mut().take(table_end) {
- *byte = true;
- }
-
- for (idx, entry) in self.entries.iter().enumerate() {
- let id = u32::try_from(idx).map_err(|_| Error::IntegerOverflow)?;
- let packed = self.load_packed(EntryId(id))?.packed;
- let start =
- usize::try_from(entry.data_offset_raw).map_err(|_| Error::IntegerOverflow)?;
- for (offset, byte) in packed.iter().copied().enumerate() {
- let pos = start.checked_add(offset).ok_or(Error::IntegerOverflow)?;
- if pos >= out.len() {
- return Err(Error::PackedSizePastEof {
- id,
- offset: u64::from(entry.data_offset_raw),
- packed_size: entry.packed_size_declared,
- file_len: u64::try_from(out.len()).map_err(|_| Error::IntegerOverflow)?,
- });
- }
- if occupied[pos] && out[pos] != byte {
- return Err(Error::CorruptEntryTable("packed payload overlap conflict"));
- }
- out[pos] = byte;
- occupied[pos] = true;
- }
- }
-
- if let Some(trailer) = &self.ao_trailer {
- out.extend_from_slice(&trailer.raw);
- }
- Ok(out)
- }
-}
-
-fn decode_payload(
- packed: &[u8],
- method: PackMethod,
- key16: u16,
- unpacked_size: u32,
-) -> Result<Vec<u8>> {
- let expected = usize::try_from(unpacked_size).map_err(|_| Error::IntegerOverflow)?;
-
- let out = match method {
- PackMethod::None => {
- if packed.len() < expected {
- return Err(Error::OutputSizeMismatch {
- expected: unpacked_size,
- got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
- });
- }
- packed[..expected].to_vec()
- }
- PackMethod::XorOnly => {
- if packed.len() < expected {
- return Err(Error::OutputSizeMismatch {
- expected: unpacked_size,
- got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
- });
- }
- xor_stream(&packed[..expected], key16)
- }
- PackMethod::Lzss => lzss_decompress_simple(packed, expected, None)?,
- PackMethod::XorLzss => {
- // Optimized: XOR on-the-fly during decompression instead of creating temp buffer
- lzss_decompress_simple(packed, expected, Some(key16))?
- }
- PackMethod::LzssHuffman => lzss_huffman_decompress(packed, expected, None)?,
- PackMethod::XorLzssHuffman => {
- // Optimized: XOR on-the-fly during decompression
- lzss_huffman_decompress(packed, expected, Some(key16))?
- }
- PackMethod::Deflate => decode_deflate(packed)?,
- PackMethod::Unknown(raw) => return Err(Error::UnsupportedMethod { raw }),
- };
-
- if out.len() != expected {
- return Err(Error::OutputSizeMismatch {
- expected: unpacked_size,
- got: u32::try_from(out.len()).unwrap_or(u32::MAX),
- });
- }
-
- Ok(out)
-}
-
-fn needs_xor_key(method: PackMethod) -> bool {
- matches!(
- method,
- PackMethod::XorOnly | PackMethod::XorLzss | PackMethod::XorLzssHuffman
- )
-}
-
-fn saturating_u32_len(len: usize) -> u32 {
- u32::try_from(len).unwrap_or(u32::MAX)
-}
-
-#[cfg(test)]
-mod tests;
diff --git a/crates/rsli/src/parse.rs b/crates/rsli/src/parse.rs
deleted file mode 100644
index d3afcd9..0000000
--- a/crates/rsli/src/parse.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-use crate::compress::xor::xor_stream;
-use crate::error::Error;
-use crate::{
- AoTrailer, EntryMeta, EntryRecord, Library, LibraryHeader, OpenOptions, PackMethod, Result,
-};
-use std::cmp::Ordering;
-use std::sync::Arc;
-
-pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
- if bytes.len() < 32 {
- return Err(Error::EntryTableOutOfBounds {
- table_offset: 32,
- table_len: 0,
- file_len: u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?,
- });
- }
-
- let mut header_raw = [0u8; 32];
- header_raw.copy_from_slice(&bytes[0..32]);
-
- let mut magic = [0u8; 2];
- magic.copy_from_slice(&bytes[0..2]);
- if &magic != b"NL" {
- let mut got = [0u8; 2];
- got.copy_from_slice(&bytes[0..2]);
- return Err(Error::InvalidMagic { got });
- }
- let reserved = bytes[2];
- let version = bytes[3];
- if version != 0x01 {
- return Err(Error::UnsupportedVersion { got: version });
- }
-
- let entry_count = i16::from_le_bytes([bytes[4], bytes[5]]);
- if entry_count < 0 {
- return Err(Error::InvalidEntryCount { got: entry_count });
- }
- let count = usize::try_from(entry_count).map_err(|_| Error::IntegerOverflow)?;
-
- // Validate entry_count fits in u32 (required for EntryId)
- if count > u32::MAX as usize {
- return Err(Error::TooManyEntries { got: count });
- }
-
- let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
- let xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
- let header = LibraryHeader {
- raw: header_raw,
- magic,
- reserved,
- version,
- entry_count,
- presorted_flag,
- xor_seed,
- };
-
- let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
- let table_offset = 32usize;
- let table_end = table_offset
- .checked_add(table_len)
- .ok_or(Error::IntegerOverflow)?;
- if table_end > bytes.len() {
- return Err(Error::EntryTableOutOfBounds {
- table_offset: u64::try_from(table_offset).map_err(|_| Error::IntegerOverflow)?,
- table_len: u64::try_from(table_len).map_err(|_| Error::IntegerOverflow)?,
- file_len: u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?,
- });
- }
-
- let table_enc = &bytes[table_offset..table_end];
- let table_plain_original = xor_stream(table_enc, (xor_seed & 0xFFFF) as u16);
- if table_plain_original.len() != table_len {
- return Err(Error::EntryTableDecryptFailed);
- }
-
- let (overlay, trailer_raw) = parse_ao_trailer(&bytes, opts.allow_ao_trailer)?;
-
- let mut entries = Vec::with_capacity(count);
- for idx in 0..count {
- let row = &table_plain_original[idx * 32..(idx + 1) * 32];
-
- let mut name_raw = [0u8; 12];
- name_raw.copy_from_slice(&row[0..12]);
- let mut service_tail = [0u8; 4];
- service_tail.copy_from_slice(&row[12..16]);
-
- let flags_signed = i16::from_le_bytes([row[16], row[17]]);
- let sort_to_original = i16::from_le_bytes([row[18], row[19]]);
- let unpacked_size = u32::from_le_bytes([row[20], row[21], row[22], row[23]]);
- let data_offset_raw = u32::from_le_bytes([row[24], row[25], row[26], row[27]]);
- let packed_size_declared = u32::from_le_bytes([row[28], row[29], row[30], row[31]]);
-
- let method_raw = (flags_signed as u16 as u32) & 0x1E0;
- let method = parse_method(method_raw);
-
- let effective_offset_u64 = u64::from(data_offset_raw)
- .checked_add(u64::from(overlay))
- .ok_or(Error::IntegerOverflow)?;
- let effective_offset =
- usize::try_from(effective_offset_u64).map_err(|_| Error::IntegerOverflow)?;
-
- let packed_size_usize =
- usize::try_from(packed_size_declared).map_err(|_| Error::IntegerOverflow)?;
- let mut packed_size_available = packed_size_usize;
-
- let end = effective_offset_u64
- .checked_add(u64::from(packed_size_declared))
- .ok_or(Error::IntegerOverflow)?;
- let file_len_u64 = u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?;
-
- if end > file_len_u64 {
- if method_raw == 0x100 && end == file_len_u64 + 1 {
- if opts.allow_deflate_eof_plus_one {
- packed_size_available = packed_size_available
- .checked_sub(1)
- .ok_or(Error::IntegerOverflow)?;
- } else {
- return Err(Error::DeflateEofPlusOneQuirkRejected {
- id: u32::try_from(idx).map_err(|_| Error::IntegerOverflow)?,
- });
- }
- } else {
- return Err(Error::PackedSizePastEof {
- id: u32::try_from(idx).map_err(|_| Error::IntegerOverflow)?,
- offset: effective_offset_u64,
- packed_size: packed_size_declared,
- file_len: file_len_u64,
- });
- }
- }
-
- let available_end = effective_offset
- .checked_add(packed_size_available)
- .ok_or(Error::IntegerOverflow)?;
- if available_end > bytes.len() {
- return Err(Error::EntryDataOutOfBounds {
- id: u32::try_from(idx).map_err(|_| Error::IntegerOverflow)?,
- offset: effective_offset_u64,
- size: packed_size_declared,
- file_len: file_len_u64,
- });
- }
-
- let name = decode_name(c_name_bytes(&name_raw));
-
- entries.push(EntryRecord {
- meta: EntryMeta {
- name,
- flags: i32::from(flags_signed),
- method,
- data_offset: effective_offset_u64,
- packed_size: packed_size_declared,
- unpacked_size,
- },
- name_raw,
- service_tail,
- sort_to_original,
- key16: sort_to_original as u16,
- data_offset_raw,
- packed_size_declared,
- packed_size_available,
- effective_offset,
- });
- }
-
- if presorted_flag == 0xABBA {
- let mut seen = vec![false; count];
- for entry in &entries {
- let idx = i32::from(entry.sort_to_original);
- if idx < 0 {
- return Err(Error::CorruptEntryTable(
- "sort_to_original is not a valid permutation index",
- ));
- }
- let idx = usize::try_from(idx).map_err(|_| Error::IntegerOverflow)?;
- if idx >= count {
- return Err(Error::CorruptEntryTable(
- "sort_to_original is not a valid permutation index",
- ));
- }
- if seen[idx] {
- return Err(Error::CorruptEntryTable(
- "sort_to_original is not a permutation",
- ));
- }
- seen[idx] = true;
- }
- if seen.iter().any(|value| !*value) {
- return Err(Error::CorruptEntryTable(
- "sort_to_original is not a permutation",
- ));
- }
- } else {
- let mut sorted: Vec<usize> = (0..count).collect();
- sorted.sort_by(|a, b| {
- cmp_c_string(
- c_name_bytes(&entries[*a].name_raw),
- c_name_bytes(&entries[*b].name_raw),
- )
- });
- for (idx, entry) in entries.iter_mut().enumerate() {
- entry.sort_to_original =
- i16::try_from(sorted[idx]).map_err(|_| Error::IntegerOverflow)?;
- entry.key16 = entry.sort_to_original as u16;
- }
- }
-
- #[cfg(test)]
- let source_size = bytes.len();
-
- Ok(Library {
- bytes,
- entries,
- header,
- ao_trailer: trailer_raw.map(|raw| AoTrailer { raw, overlay }),
- #[cfg(test)]
- table_plain_original,
- #[cfg(test)]
- source_size,
- })
-}
-
-fn parse_ao_trailer(bytes: &[u8], allow: bool) -> Result<(u32, Option<[u8; 6]>)> {
- if !allow || bytes.len() < 6 {
- return Ok((0, None));
- }
-
- if &bytes[bytes.len() - 6..bytes.len() - 4] != b"AO" {
- return Ok((0, None));
- }
-
- let mut trailer = [0u8; 6];
- trailer.copy_from_slice(&bytes[bytes.len() - 6..]);
- let overlay = u32::from_le_bytes([trailer[2], trailer[3], trailer[4], trailer[5]]);
-
- if u64::from(overlay) > u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)? {
- return Err(Error::MediaOverlayOutOfBounds {
- overlay,
- file_len: u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?,
- });
- }
-
- Ok((overlay, Some(trailer)))
-}
-
-pub fn parse_method(raw: u32) -> PackMethod {
- match raw {
- 0x000 => PackMethod::None,
- 0x020 => PackMethod::XorOnly,
- 0x040 => PackMethod::Lzss,
- 0x060 => PackMethod::XorLzss,
- 0x080 => PackMethod::LzssHuffman,
- 0x0A0 => PackMethod::XorLzssHuffman,
- 0x100 => PackMethod::Deflate,
- other => PackMethod::Unknown(other),
- }
-}
-
-fn decode_name(name: &[u8]) -> String {
- name.iter().map(|b| char::from(*b)).collect()
-}
-
-pub fn c_name_bytes(raw: &[u8; 12]) -> &[u8] {
- let len = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
- &raw[..len]
-}
-
-pub fn cmp_c_string(a: &[u8], b: &[u8]) -> Ordering {
- let min_len = a.len().min(b.len());
- let mut idx = 0usize;
- while idx < min_len {
- if a[idx] != b[idx] {
- return a[idx].cmp(&b[idx]);
- }
- idx += 1;
- }
- a.len().cmp(&b.len())
-}
diff --git a/crates/rsli/src/tests.rs b/crates/rsli/src/tests.rs
deleted file mode 100644
index ffd611d..0000000
--- a/crates/rsli/src/tests.rs
+++ /dev/null
@@ -1,1338 +0,0 @@
-use super::*;
-use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
-use crate::compress::xor::xor_stream;
-use common::collect_files_recursive;
-use flate2::write::DeflateEncoder;
-use flate2::write::ZlibEncoder;
-use flate2::Compression;
-use proptest::prelude::*;
-use std::any::Any;
-use std::fs;
-use std::io::Write as _;
-use std::panic::{catch_unwind, AssertUnwindSafe};
-use std::path::PathBuf;
-use std::sync::Arc;
-
-#[derive(Clone, Debug)]
-struct SyntheticRsliEntry {
- name: String,
- method_raw: u16,
- plain: Vec<u8>,
- declared_packed_size: Option<u32>,
-}
-
-#[derive(Clone, Debug)]
-struct RsliBuildOptions {
- seed: u32,
- presorted: bool,
- overlay: u32,
- add_ao_trailer: bool,
-}
-
-impl Default for RsliBuildOptions {
- fn default() -> Self {
- Self {
- seed: 0x1234_5678,
- presorted: true,
- overlay: 0,
- add_ao_trailer: false,
- }
- }
-}
-
-fn rsli_test_files() -> Vec<PathBuf> {
- let root = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("..")
- .join("..")
- .join("testdata")
- .join("rsli");
- let mut files = Vec::new();
- collect_files_recursive(&root, &mut files);
- files.sort();
- files
- .into_iter()
- .filter(|path| {
- fs::read(path)
- .map(|data| data.get(0..4) == Some(b"NL\0\x01"))
- .unwrap_or(false)
- })
- .collect()
-}
-
-fn panic_message(payload: Box<dyn Any + Send>) -> String {
- let any = payload.as_ref();
- if let Some(message) = any.downcast_ref::<String>() {
- return message.clone();
- }
- if let Some(message) = any.downcast_ref::<&str>() {
- return (*message).to_string();
- }
- String::from("panic without message")
-}
-
-fn write_temp_file(prefix: &str, bytes: &[u8]) -> PathBuf {
- let mut path = std::env::temp_dir();
- path.push(format!(
- "{}-{}-{}.bin",
- prefix,
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map(|d| d.as_nanos())
- .unwrap_or(0)
- ));
- fs::write(&path, bytes).expect("failed to write temp archive");
- path
-}
-
-fn deflate_raw(data: &[u8]) -> Vec<u8> {
- let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
- encoder
- .write_all(data)
- .expect("deflate encoder write failed");
- encoder.finish().expect("deflate encoder finish failed")
-}
-
-fn deflate_zlib(data: &[u8]) -> Vec<u8> {
- let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
- encoder.write_all(data).expect("zlib encoder write failed");
- encoder.finish().expect("zlib encoder finish failed")
-}
-
-fn lzss_pack_literals(data: &[u8]) -> Vec<u8> {
- let mut out = Vec::new();
- for chunk in data.chunks(8) {
- let mask = if chunk.len() == 8 {
- 0xFF
- } else {
- (1u16
- .checked_shl(u32::try_from(chunk.len()).expect("chunk len overflow"))
- .expect("shift overflow")
- - 1) as u8
- };
- out.push(mask);
- out.extend_from_slice(chunk);
- }
- out
-}
-
-struct BitWriter {
- bytes: Vec<u8>,
- current: u8,
- mask: u8,
-}
-
-impl BitWriter {
- fn new() -> Self {
- Self {
- bytes: Vec::new(),
- current: 0,
- mask: 0x80,
- }
- }
-
- fn write_bit(&mut self, bit: u8) {
- if bit != 0 {
- self.current |= self.mask;
- }
- self.mask >>= 1;
- if self.mask == 0 {
- self.bytes.push(self.current);
- self.current = 0;
- self.mask = 0x80;
- }
- }
-
- fn finish(mut self) -> Vec<u8> {
- if self.mask != 0x80 {
- self.bytes.push(self.current);
- }
- self.bytes
- }
-}
-
-struct LzhLiteralModel {
- freq: [u16; LZH_T + 1],
- parent: [usize; LZH_T + LZH_N_CHAR],
- son: [usize; LZH_T + 1],
-}
-
-impl LzhLiteralModel {
- fn new() -> Self {
- let mut model = Self {
- freq: [0; LZH_T + 1],
- parent: [0; LZH_T + LZH_N_CHAR],
- son: [0; LZH_T + 1],
- };
- model.start_huff();
- model
- }
-
- fn encode_literal(&mut self, literal: u8, writer: &mut BitWriter) {
- let target = usize::from(literal) + LZH_T;
- let mut path = Vec::new();
- let mut visited = [false; LZH_T + 1];
- let found = self.find_path(self.son[LZH_R], target, &mut path, &mut visited);
- assert!(found, "failed to encode literal {literal}");
- for bit in path {
- writer.write_bit(bit);
- }
-
- self.update(usize::from(literal));
- }
-
- fn find_path(
- &self,
- node: usize,
- target: usize,
- path: &mut Vec<u8>,
- visited: &mut [bool; LZH_T + 1],
- ) -> bool {
- if node == target {
- return true;
- }
- if node >= LZH_T {
- return false;
- }
- if visited[node] {
- return false;
- }
- visited[node] = true;
-
- for bit in [0u8, 1u8] {
- let child = self.son[node + usize::from(bit)];
- path.push(bit);
- if self.find_path(child, target, path, visited) {
- visited[node] = false;
- return true;
- }
- path.pop();
- }
-
- visited[node] = false;
- false
- }
-
- fn start_huff(&mut self) {
- for i in 0..LZH_N_CHAR {
- self.freq[i] = 1;
- self.son[i] = i + LZH_T;
- self.parent[i + LZH_T] = i;
- }
-
- let mut i = 0usize;
- let mut j = LZH_N_CHAR;
- while j <= LZH_R {
- self.freq[j] = self.freq[i].saturating_add(self.freq[i + 1]);
- self.son[j] = i;
- self.parent[i] = j;
- self.parent[i + 1] = j;
- i += 2;
- j += 1;
- }
-
- self.freq[LZH_T] = u16::MAX;
- self.parent[LZH_R] = 0;
- }
-
- fn update(&mut self, c: usize) {
- if self.freq[LZH_R] == LZH_MAX_FREQ {
- self.reconstruct();
- }
-
- let mut current = self.parent[c + LZH_T];
- loop {
- self.freq[current] = self.freq[current].saturating_add(1);
- let freq = self.freq[current];
-
- if current + 1 < self.freq.len() && freq > self.freq[current + 1] {
- let mut swap_idx = current + 1;
- while swap_idx + 1 < self.freq.len() && freq > self.freq[swap_idx + 1] {
- swap_idx += 1;
- }
-
- self.freq.swap(current, swap_idx);
-
- let left = self.son[current];
- let right = self.son[swap_idx];
- self.son[current] = right;
- self.son[swap_idx] = left;
-
- self.parent[left] = swap_idx;
- if left < LZH_T {
- self.parent[left + 1] = swap_idx;
- }
-
- self.parent[right] = current;
- if right < LZH_T {
- self.parent[right + 1] = current;
- }
-
- current = swap_idx;
- }
-
- current = self.parent[current];
- if current == 0 {
- break;
- }
- }
- }
-
- fn reconstruct(&mut self) {
- let mut j = 0usize;
- for i in 0..LZH_T {
- if self.son[i] >= LZH_T {
- self.freq[j] = self.freq[i].div_ceil(2);
- self.son[j] = self.son[i];
- j += 1;
- }
- }
-
- let mut i = 0usize;
- let mut current = LZH_N_CHAR;
- while current < LZH_T {
- let sum = self.freq[i].saturating_add(self.freq[i + 1]);
- self.freq[current] = sum;
-
- let mut insert_at = current;
- while insert_at > 0 && sum < self.freq[insert_at - 1] {
- insert_at -= 1;
- }
-
- for move_idx in (insert_at..current).rev() {
- self.freq[move_idx + 1] = self.freq[move_idx];
- self.son[move_idx + 1] = self.son[move_idx];
- }
-
- self.freq[insert_at] = sum;
- self.son[insert_at] = i;
- i += 2;
- current += 1;
- }
-
- for idx in 0..LZH_T {
- let node = self.son[idx];
- self.parent[node] = idx;
- if node < LZH_T {
- self.parent[node + 1] = idx;
- }
- }
-
- self.freq[LZH_T] = u16::MAX;
- self.parent[LZH_R] = 0;
- }
-}
-
-fn lzh_pack_literals(data: &[u8]) -> Vec<u8> {
- let mut writer = BitWriter::new();
- let mut model = LzhLiteralModel::new();
- for byte in data {
- model.encode_literal(*byte, &mut writer);
- }
- writer.finish()
-}
-
-fn packed_for_method(method_raw: u16, plain: &[u8], key16: u16) -> Vec<u8> {
- match (u32::from(method_raw)) & 0x1E0 {
- 0x000 => plain.to_vec(),
- 0x020 => xor_stream(plain, key16),
- 0x040 => lzss_pack_literals(plain),
- 0x060 => xor_stream(&lzss_pack_literals(plain), key16),
- 0x080 => lzh_pack_literals(plain),
- 0x0A0 => xor_stream(&lzh_pack_literals(plain), key16),
- 0x100 => deflate_raw(plain),
- _ => plain.to_vec(),
- }
-}
-
-fn build_rsli_bytes(entries: &[SyntheticRsliEntry], opts: &RsliBuildOptions) -> Vec<u8> {
- let count = entries.len();
- let mut rows_plain = vec![0u8; count * 32];
- let table_end = 32 + rows_plain.len();
-
- let mut sort_lookup: Vec<usize> = (0..count).collect();
- sort_lookup.sort_by(|a, b| entries[*a].name.as_bytes().cmp(entries[*b].name.as_bytes()));
-
- let mut packed_blobs = Vec::with_capacity(count);
- for index in 0..count {
- let key16 = u16::try_from(sort_lookup[index]).expect("sort index overflow");
- let packed = packed_for_method(entries[index].method_raw, &entries[index].plain, key16);
- packed_blobs.push(packed);
- }
-
- let overlay = usize::try_from(opts.overlay).expect("overlay overflow");
- let mut cursor = table_end + overlay;
- let mut output = vec![0u8; cursor];
-
- let mut data_offsets = Vec::with_capacity(count);
- for (index, packed) in packed_blobs.iter().enumerate() {
- let raw_offset = cursor
- .checked_sub(overlay)
- .expect("overlay larger than cursor");
- data_offsets.push(raw_offset);
-
- let end = cursor.checked_add(packed.len()).expect("cursor overflow");
- if output.len() < end {
- output.resize(end, 0);
- }
- output[cursor..end].copy_from_slice(packed);
- cursor = end;
-
- let base = index * 32;
- let mut name_raw = [0u8; 12];
- let uppercase = entries[index].name.to_ascii_uppercase();
- let name_bytes = uppercase.as_bytes();
- assert!(name_bytes.len() <= 12, "name too long in synthetic fixture");
- name_raw[..name_bytes.len()].copy_from_slice(name_bytes);
-
- rows_plain[base..base + 12].copy_from_slice(&name_raw);
-
- let sort_field: i16 = if opts.presorted {
- i16::try_from(sort_lookup[index]).expect("sort field overflow")
- } else {
- 0
- };
-
- let packed_size = entries[index]
- .declared_packed_size
- .unwrap_or_else(|| u32::try_from(packed.len()).expect("packed size overflow"));
-
- rows_plain[base + 16..base + 18].copy_from_slice(&entries[index].method_raw.to_le_bytes());
- rows_plain[base + 18..base + 20].copy_from_slice(&sort_field.to_le_bytes());
- rows_plain[base + 20..base + 24].copy_from_slice(
- &u32::try_from(entries[index].plain.len())
- .expect("unpacked size overflow")
- .to_le_bytes(),
- );
- rows_plain[base + 24..base + 28].copy_from_slice(
- &u32::try_from(data_offsets[index])
- .expect("data offset overflow")
- .to_le_bytes(),
- );
- rows_plain[base + 28..base + 32].copy_from_slice(&packed_size.to_le_bytes());
- }
-
- if output.len() < table_end {
- output.resize(table_end, 0);
- }
-
- output[0..2].copy_from_slice(b"NL");
- output[2] = 0;
- output[3] = 1;
- output[4..6].copy_from_slice(
- &i16::try_from(count)
- .expect("entry count overflow")
- .to_le_bytes(),
- );
-
- let presorted_flag = if opts.presorted { 0xABBA_u16 } else { 0_u16 };
- output[14..16].copy_from_slice(&presorted_flag.to_le_bytes());
- output[20..24].copy_from_slice(&opts.seed.to_le_bytes());
-
- let encrypted_table = xor_stream(&rows_plain, (opts.seed & 0xFFFF) as u16);
- output[32..table_end].copy_from_slice(&encrypted_table);
-
- if opts.add_ao_trailer {
- output.extend_from_slice(b"AO");
- output.extend_from_slice(&opts.overlay.to_le_bytes());
- }
-
- output
-}
-
-fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
- let slice = bytes
- .get(offset..offset + 4)
- .expect("u32 read out of bounds in test");
- let arr: [u8; 4] = slice.try_into().expect("u32 conversion failed in test");
- u32::from_le_bytes(arr)
-}
-
-#[test]
-fn rsli_read_unpack_and_repack_all_files() {
- let files = rsli_test_files();
- if files.is_empty() {
- eprintln!(
- "skipping rsli_read_unpack_and_repack_all_files: no RsLi archives in testdata/rsli"
- );
- return;
- }
-
- let checked = files.len();
- let mut success = 0usize;
- let mut failures = Vec::new();
-
- for path in files {
- let display_path = path.display().to_string();
- let result = catch_unwind(AssertUnwindSafe(|| {
- let original = fs::read(&path).expect("failed to read archive");
- let library = Library::open_path(&path)
- .unwrap_or_else(|err| panic!("failed to open {}: {err}", path.display()));
-
- let count = library.entry_count();
- assert_eq!(
- count,
- library.entries().count(),
- "entry count mismatch: {}",
- path.display()
- );
-
- for idx in 0..count {
- let id = EntryId(idx as u32);
- let meta_ref = library
- .get(id)
- .unwrap_or_else(|| panic!("missing entry #{idx} in {}", path.display()));
-
- let loaded = library.load(id).unwrap_or_else(|err| {
- panic!("load failed for {} entry #{idx}: {err}", path.display())
- });
-
- let packed = library.load_packed(id).unwrap_or_else(|err| {
- panic!(
- "load_packed failed for {} entry #{idx}: {err}",
- path.display()
- )
- });
- let unpacked = library.unpack(&packed).unwrap_or_else(|err| {
- panic!("unpack failed for {} entry #{idx}: {err}", path.display())
- });
- assert_eq!(
- loaded,
- unpacked,
- "load != unpack in {} entry #{idx}",
- path.display()
- );
-
- let mut out = Vec::new();
- let written = library.load_into(id, &mut out).unwrap_or_else(|err| {
- panic!(
- "load_into failed for {} entry #{idx}: {err}",
- path.display()
- )
- });
- assert_eq!(
- written,
- loaded.len(),
- "load_into size mismatch in {} entry #{idx}",
- path.display()
- );
- assert_eq!(
- out,
- loaded,
- "load_into payload mismatch in {} entry #{idx}",
- path.display()
- );
-
- let fast = library.load_fast(id).unwrap_or_else(|err| {
- panic!(
- "load_fast failed for {} entry #{idx}: {err}",
- path.display()
- )
- });
- assert_eq!(
- fast.as_slice(),
- loaded.as_slice(),
- "load_fast mismatch in {} entry #{idx}",
- path.display()
- );
-
- let found = library.find(&meta_ref.meta.name).unwrap_or_else(|| {
- panic!(
- "find failed for '{}' in {}",
- meta_ref.meta.name,
- path.display()
- )
- });
- let found_meta = library.get(found).expect("find returned invalid entry id");
- assert_eq!(
- found_meta.meta.name,
- meta_ref.meta.name,
- "find returned a different entry in {}",
- path.display()
- );
- }
-
- let rebuilt = library
- .rebuild_from_parsed_metadata()
- .unwrap_or_else(|err| panic!("rebuild failed for {}: {err}", path.display()));
- assert_eq!(
- rebuilt,
- original,
- "byte-to-byte roundtrip mismatch for {}",
- path.display()
- );
- }));
-
- match result {
- Ok(()) => success += 1,
- Err(payload) => failures.push(format!("{}: {}", display_path, panic_message(payload))),
- }
- }
-
- let failed = failures.len();
- eprintln!(
- "RsLi summary: checked={}, success={}, failed={}",
- checked, success, failed
- );
- if !failures.is_empty() {
- panic!(
- "RsLi validation failed.\nsummary: checked={}, success={}, failed={}\n{}",
- checked,
- success,
- failed,
- failures.join("\n")
- );
- }
-}
-
-#[test]
-fn rsli_docs_structural_invariants_all_files() {
- let files = rsli_test_files();
- if files.is_empty() {
- eprintln!(
- "skipping rsli_docs_structural_invariants_all_files: no RsLi archives in testdata/rsli"
- );
- return;
- }
-
- let mut deflate_eof_plus_one_quirks = Vec::new();
-
- for path in files {
- let bytes = fs::read(&path).unwrap_or_else(|err| {
- panic!("failed to read {}: {err}", path.display());
- });
-
- assert!(
- bytes.len() >= 32,
- "RsLi header too short in {}",
- path.display()
- );
- assert_eq!(&bytes[0..2], b"NL", "bad magic in {}", path.display());
- assert_eq!(
- bytes[2],
- 0,
- "reserved header byte must be zero in {}",
- path.display()
- );
- assert_eq!(bytes[3], 1, "bad version in {}", path.display());
-
- let entry_count = i16::from_le_bytes([bytes[4], bytes[5]]);
- assert!(
- entry_count >= 0,
- "negative entry_count={} in {}",
- entry_count,
- path.display()
- );
- let count = usize::try_from(entry_count).expect("entry_count overflow");
- let table_size = count.checked_mul(32).expect("table_size overflow");
- let table_end = 32usize.checked_add(table_size).expect("table_end overflow");
- assert!(
- table_end <= bytes.len(),
- "table out of bounds in {}",
- path.display()
- );
-
- let seed = read_u32_le(&bytes, 20);
- let table_plain = xor_stream(&bytes[32..table_end], (seed & 0xFFFF) as u16);
- assert_eq!(
- table_plain.len(),
- table_size,
- "decrypted table size mismatch in {}",
- path.display()
- );
-
- let mut overlay = 0u32;
- if bytes.len() >= 6 && &bytes[bytes.len() - 6..bytes.len() - 4] == b"AO" {
- overlay = read_u32_le(&bytes, bytes.len() - 4);
- assert!(
- usize::try_from(overlay).expect("overlay overflow") <= bytes.len(),
- "overlay beyond EOF in {}",
- path.display()
- );
- }
-
- let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
- let mut sort_values = Vec::with_capacity(count);
-
- for index in 0..count {
- let base = index * 32;
- let row = &table_plain[base..base + 32];
- let flags_signed = i16::from_le_bytes([row[16], row[17]]);
- let sort_to_original = i16::from_le_bytes([row[18], row[19]]);
- let data_offset = u64::from(read_u32_le(row, 24));
- let packed_size = u64::from(read_u32_le(row, 28));
-
- let method = (flags_signed as u16 as u32) & 0x1E0;
- let effective_offset = data_offset + u64::from(overlay);
- let end = effective_offset + packed_size;
- let file_len = u64::try_from(bytes.len()).expect("file size overflow");
-
- if end > file_len {
- assert!(
- method == 0x100 && end == file_len + 1,
- "packed range out of bounds in {} entry #{index}: method=0x{method:03X}, range=[{effective_offset}, {end}), file={file_len}",
- path.display()
- );
- deflate_eof_plus_one_quirks.push((path.display().to_string(), index));
- }
-
- sort_values.push(sort_to_original);
- }
-
- if presorted_flag == 0xABBA {
- let mut sorted = sort_values;
- sorted.sort_unstable();
- let expected: Vec<i16> = (0..count)
- .map(|idx| i16::try_from(idx).expect("too many entries for i16"))
- .collect();
- assert_eq!(
- sorted,
- expected,
- "sort_to_original is not a permutation in {}",
- path.display()
- );
- }
- }
-
- if !deflate_eof_plus_one_quirks.is_empty() {
- assert!(
- deflate_eof_plus_one_quirks
- .iter()
- .all(|(file, idx)| file.ends_with("sprites.lib") && *idx == 23),
- "unexpected deflate EOF+1 quirks: {:?}",
- deflate_eof_plus_one_quirks
- );
- }
-}
-
-#[test]
-fn rsli_synthetic_all_methods_roundtrip() {
- let entries = vec![
- SyntheticRsliEntry {
- name: "M_NONE".to_string(),
- method_raw: 0x000,
- plain: b"plain-data".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_XOR".to_string(),
- method_raw: 0x020,
- plain: b"xor-only".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_LZSS".to_string(),
- method_raw: 0x040,
- plain: b"lzss literals payload".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_XLZS".to_string(),
- method_raw: 0x060,
- plain: b"xor lzss payload".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_LZHU".to_string(),
- method_raw: 0x080,
- plain: b"huffman literals payload".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_XLZH".to_string(),
- method_raw: 0x0A0,
- plain: b"xor huffman payload".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "M_DEFL".to_string(),
- method_raw: 0x100,
- plain: b"deflate payload with repetition repetition repetition".to_vec(),
- declared_packed_size: None,
- },
- ];
-
- let bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- seed: 0xA1B2_C3D4,
- presorted: false,
- overlay: 0,
- add_ao_trailer: false,
- },
- );
- let path = write_temp_file("rsli-all-methods", &bytes);
-
- let library = Library::open_path(&path).expect("open synthetic rsli failed");
- assert_eq!(library.entry_count(), entries.len());
-
- for entry in &entries {
- let id = library
- .find(&entry.name)
- .unwrap_or_else(|| panic!("find failed for {}", entry.name));
- let loaded = library
- .load(id)
- .unwrap_or_else(|err| panic!("load failed for {}: {err}", entry.name));
- assert_eq!(
- loaded, entry.plain,
- "decoded payload mismatch for {}",
- entry.name
- );
-
- let packed = library
- .load_packed(id)
- .unwrap_or_else(|err| panic!("load_packed failed for {}: {err}", entry.name));
- let unpacked = library
- .unpack(&packed)
- .unwrap_or_else(|err| panic!("unpack failed for {}: {err}", entry.name));
- assert_eq!(unpacked, entry.plain, "unpack mismatch for {}", entry.name);
- }
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_empty_archive_roundtrip() {
- let bytes = build_rsli_bytes(&[], &RsliBuildOptions::default());
- let path = write_temp_file("rsli-empty", &bytes);
-
- let library = Library::open_path(&path).expect("open empty rsli failed");
- assert_eq!(library.entry_count(), 0);
- assert_eq!(library.find("ANYTHING"), None);
-
- let rebuilt = library
- .rebuild_from_parsed_metadata()
- .expect("rebuild empty rsli failed");
- assert_eq!(rebuilt, bytes, "empty rsli roundtrip mismatch");
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_max_name_length_without_nul_roundtrip() {
- let max_name = "NAME12345678";
- assert_eq!(max_name.len(), 12);
-
- let bytes = build_rsli_bytes(
- &[SyntheticRsliEntry {
- name: max_name.to_string(),
- method_raw: 0x000,
- plain: b"payload".to_vec(),
- declared_packed_size: None,
- }],
- &RsliBuildOptions::default(),
- );
- let path = write_temp_file("rsli-max-name", &bytes);
-
- let library = Library::open_path(&path).expect("open max-name rsli failed");
- assert_eq!(library.entry_count(), 1);
- assert_eq!(library.find(max_name), Some(EntryId(0)));
- assert_eq!(
- library.find(&max_name.to_ascii_lowercase()),
- Some(EntryId(0))
- );
- assert_eq!(
- library.entries[0]
- .name_raw
- .iter()
- .position(|byte| *byte == 0),
- None,
- "name_raw must occupy full 12 bytes without NUL"
- );
-
- let entry = library.get(EntryId(0)).expect("missing entry");
- assert_eq!(entry.meta.name, max_name);
- assert_eq!(
- library.load(EntryId(0)).expect("load failed"),
- b"payload",
- "payload mismatch"
- );
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_lzss_large_payload_over_4k_roundtrip() {
- let plain: Vec<u8> = (0..10_000u32).map(|v| (v % 251) as u8).collect();
- let entries = vec![
- SyntheticRsliEntry {
- name: "LZSS4K".to_string(),
- method_raw: 0x040,
- plain: plain.clone(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "XLZS4K".to_string(),
- method_raw: 0x060,
- plain: plain.clone(),
- declared_packed_size: None,
- },
- ];
- let bytes = build_rsli_bytes(&entries, &RsliBuildOptions::default());
- let path = write_temp_file("rsli-lzss-4k", &bytes);
-
- let library = Library::open_path(&path).expect("open large-lzss rsli failed");
- assert_eq!(library.entry_count(), entries.len());
-
- for entry in &entries {
- let id = library
- .find(&entry.name)
- .unwrap_or_else(|| panic!("find failed for {}", entry.name));
- let loaded = library
- .load(id)
- .unwrap_or_else(|err| panic!("load failed for {}: {err}", entry.name));
- assert_eq!(loaded, plain, "payload mismatch for {}", entry.name);
- }
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_find_falls_back_when_sort_table_corrupted_in_memory() {
- let entries = vec![
- SyntheticRsliEntry {
- name: "AAA".to_string(),
- method_raw: 0x000,
- plain: b"a".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "BBB".to_string(),
- method_raw: 0x000,
- plain: b"b".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "CCC".to_string(),
- method_raw: 0x000,
- plain: b"c".to_vec(),
- declared_packed_size: None,
- },
- ];
- let bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- presorted: true,
- ..RsliBuildOptions::default()
- },
- );
- let path = write_temp_file("rsli-find-fallback", &bytes);
-
- let mut library = Library::open_path(&path).expect("open synthetic rsli failed");
- library.entries[1].sort_to_original = -1;
-
- assert_eq!(library.find("AAA"), Some(EntryId(0)));
- assert_eq!(library.find("bbb"), Some(EntryId(1)));
- assert_eq!(library.find("CcC"), Some(EntryId(2)));
- assert_eq!(library.find("missing"), None);
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_deflate_method_rejects_zlib_wrapped_stream() {
- let plain = b"payload".to_vec();
- let zlib_payload = deflate_zlib(&plain);
- let entries = vec![SyntheticRsliEntry {
- name: "ZLIB".to_string(),
- method_raw: 0x100,
- plain,
- declared_packed_size: Some(
- u32::try_from(zlib_payload.len()).expect("zlib payload size overflow"),
- ),
- }];
- let mut bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- presorted: true,
- ..RsliBuildOptions::default()
- },
- );
-
- let table_end = 32 + entries.len() * 32;
- let data_offset = table_end;
- let data_end = data_offset + zlib_payload.len();
- if bytes.len() < data_end {
- bytes.resize(data_end, 0);
- }
- bytes[data_offset..data_end].copy_from_slice(&zlib_payload);
-
- let path = write_temp_file("rsli-zlib-reject", &bytes);
- let library = Library::open_path(&path).expect("open zlib-wrapped rsli failed");
- match library.load(EntryId(0)) {
- Err(Error::DecompressionFailed(reason)) => {
- assert_eq!(reason, "deflate");
- }
- other => panic!("expected deflate decompression error, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_lzss_huffman_reports_unexpected_eof() {
- let entries = vec![SyntheticRsliEntry {
- name: "TRUNC".to_string(),
- method_raw: 0x080,
- plain: b"this payload is long enough".to_vec(),
- declared_packed_size: None,
- }];
- let mut bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- presorted: true,
- ..RsliBuildOptions::default()
- },
- );
-
- let seed = read_u32_le(&bytes, 20);
- let mut table_plain = xor_stream(&bytes[32..64], (seed & 0xFFFF) as u16);
- let original_packed_size = u32::from_le_bytes([
- table_plain[28],
- table_plain[29],
- table_plain[30],
- table_plain[31],
- ]);
- assert!(
- original_packed_size > 4,
- "packed payload too small for truncation"
- );
- let truncated_size = original_packed_size - 3;
- table_plain[28..32].copy_from_slice(&truncated_size.to_le_bytes());
- let encrypted_table = xor_stream(&table_plain, (seed & 0xFFFF) as u16);
- bytes[32..64].copy_from_slice(&encrypted_table);
-
- let path = write_temp_file("rsli-lzh-truncated", &bytes);
- let library = Library::open_path(&path).expect("open truncated lzh rsli failed");
- match library.load(EntryId(0)) {
- Err(Error::DecompressionFailed(reason)) => {
- assert_eq!(reason, "lzss-huffman: unexpected EOF");
- }
- other => panic!("expected lzss-huffman EOF error, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_presorted_flag_requires_permutation() {
- let entries = vec![
- SyntheticRsliEntry {
- name: "AAA".to_string(),
- method_raw: 0x000,
- plain: b"a".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "BBB".to_string(),
- method_raw: 0x000,
- plain: b"b".to_vec(),
- declared_packed_size: None,
- },
- ];
- let mut bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- presorted: true,
- ..RsliBuildOptions::default()
- },
- );
-
- let seed = read_u32_le(&bytes, 20);
- let mut table_plain = xor_stream(&bytes[32..32 + entries.len() * 32], (seed & 0xFFFF) as u16);
-
- // Corrupt sort_to_original: duplicate index 0, so the table is not a permutation.
- table_plain[18..20].copy_from_slice(&0i16.to_le_bytes());
- table_plain[50..52].copy_from_slice(&0i16.to_le_bytes());
-
- let table_encrypted = xor_stream(&table_plain, (seed & 0xFFFF) as u16);
- bytes[32..32 + table_encrypted.len()].copy_from_slice(&table_encrypted);
-
- let path = write_temp_file("rsli-bad-presorted-perm", &bytes);
- match Library::open_path(&path) {
- Err(Error::CorruptEntryTable(message)) => {
- assert!(
- message.contains("permutation"),
- "unexpected error message: {message}"
- );
- }
- other => panic!("expected CorruptEntryTable for invalid permutation, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_load_reports_correct_entry_id_on_range_failure() {
- let entries = vec![
- SyntheticRsliEntry {
- name: "ONE".to_string(),
- method_raw: 0x000,
- plain: b"one".to_vec(),
- declared_packed_size: None,
- },
- SyntheticRsliEntry {
- name: "TWO".to_string(),
- method_raw: 0x000,
- plain: b"two".to_vec(),
- declared_packed_size: None,
- },
- ];
- let bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- presorted: true,
- ..RsliBuildOptions::default()
- },
- );
- let path = write_temp_file("rsli-entry-id-error", &bytes);
-
- let mut library = Library::open_path(&path).expect("open synthetic rsli failed");
- library.entries[1].packed_size_available = usize::MAX;
-
- match library.load(EntryId(1)) {
- Err(Error::IntegerOverflow) => {}
- other => panic!("expected IntegerOverflow, got {other:?}"),
- }
-
- library.entries[1].packed_size_available = library.bytes.len();
- match library.load(EntryId(1)) {
- Err(Error::EntryDataOutOfBounds { id, .. }) => assert_eq!(id, 1),
- other => panic!("expected EntryDataOutOfBounds with id=1, got {other:?}"),
- }
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_xorlzss_huffman_on_the_fly_roundtrip() {
- let plain: Vec<u8> = (0..512u16).map(|i| b'A' + (i % 26) as u8).collect();
- let entries = vec![SyntheticRsliEntry {
- name: "XLZH_ONFLY".to_string(),
- method_raw: 0x0A0,
- plain: plain.clone(),
- declared_packed_size: None,
- }];
-
- let bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- seed: 0x0BAD_C0DE,
- presorted: true,
- overlay: 0,
- add_ao_trailer: false,
- },
- );
- let path = write_temp_file("rsli-xorlzh-onfly", &bytes);
-
- let library = Library::open_path(&path).expect("open synthetic XLZH archive failed");
- let id = library
- .find("XLZH_ONFLY")
- .expect("find XLZH_ONFLY entry failed");
-
- let loaded = library.load(id).expect("load XLZH_ONFLY failed");
- assert_eq!(loaded, plain);
-
- let packed = library
- .load_packed(id)
- .expect("load_packed XLZH_ONFLY failed");
- let unpacked = library.unpack(&packed).expect("unpack XLZH_ONFLY failed");
- assert_eq!(unpacked, loaded);
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_synthetic_overlay_and_ao_trailer() {
- let entries = vec![SyntheticRsliEntry {
- name: "OVERLAY".to_string(),
- method_raw: 0x040,
- plain: b"overlay-data".to_vec(),
- declared_packed_size: None,
- }];
-
- let bytes = build_rsli_bytes(
- &entries,
- &RsliBuildOptions {
- seed: 0x4433_2211,
- presorted: true,
- overlay: 128,
- add_ao_trailer: true,
- },
- );
- let path = write_temp_file("rsli-overlay", &bytes);
-
- let library = Library::open_path_with(
- &path,
- OpenOptions {
- allow_ao_trailer: true,
- allow_deflate_eof_plus_one: true,
- },
- )
- .expect("open with AO trailer enabled failed");
-
- let id = library.find("OVERLAY").expect("find overlay entry failed");
- let payload = library.load(id).expect("load overlay entry failed");
- assert_eq!(payload, b"overlay-data");
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_deflate_eof_plus_one_quirk() {
- let plain = b"quirk deflate payload".to_vec();
- let packed = deflate_raw(&plain);
- let declared = u32::try_from(packed.len() + 1).expect("declared size overflow");
-
- let entries = vec![SyntheticRsliEntry {
- name: "QUIRK".to_string(),
- method_raw: 0x100,
- plain,
- declared_packed_size: Some(declared),
- }];
- let bytes = build_rsli_bytes(&entries, &RsliBuildOptions::default());
- let path = write_temp_file("rsli-deflate-quirk", &bytes);
-
- let lib_ok = Library::open_path_with(
- &path,
- OpenOptions {
- allow_ao_trailer: true,
- allow_deflate_eof_plus_one: true,
- },
- )
- .expect("open with EOF+1 quirk enabled failed");
- let loaded = lib_ok
- .load(lib_ok.find("QUIRK").expect("find quirk entry failed"))
- .expect("load quirk entry failed");
- assert_eq!(loaded, b"quirk deflate payload");
-
- match Library::open_path_with(
- &path,
- OpenOptions {
- allow_ao_trailer: true,
- allow_deflate_eof_plus_one: false,
- },
- ) {
- Err(Error::DeflateEofPlusOneQuirkRejected { id }) => assert_eq!(id, 0),
- other => panic!("expected DeflateEofPlusOneQuirkRejected, got {other:?}"),
- }
-
- let _ = fs::remove_file(&path);
-}
-
-#[test]
-fn rsli_validation_error_cases() {
- let valid = build_rsli_bytes(
- &[SyntheticRsliEntry {
- name: "BASE".to_string(),
- method_raw: 0x000,
- plain: b"abc".to_vec(),
- declared_packed_size: None,
- }],
- &RsliBuildOptions::default(),
- );
-
- let mut bad_magic = valid.clone();
- bad_magic[0..2].copy_from_slice(b"XX");
- let path = write_temp_file("rsli-bad-magic", &bad_magic);
- match Library::open_path(&path) {
- Err(Error::InvalidMagic { .. }) => {}
- other => panic!("expected InvalidMagic, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut bad_version = valid.clone();
- bad_version[3] = 2;
- let path = write_temp_file("rsli-bad-version", &bad_version);
- match Library::open_path(&path) {
- Err(Error::UnsupportedVersion { got }) => assert_eq!(got, 2),
- other => panic!("expected UnsupportedVersion, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut bad_count = valid.clone();
- bad_count[4..6].copy_from_slice(&(-1_i16).to_le_bytes());
- let path = write_temp_file("rsli-bad-count", &bad_count);
- match Library::open_path(&path) {
- Err(Error::InvalidEntryCount { got }) => assert_eq!(got, -1),
- other => panic!("expected InvalidEntryCount, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut bad_table = valid.clone();
- bad_table[4..6].copy_from_slice(&100_i16.to_le_bytes());
- let path = write_temp_file("rsli-bad-table", &bad_table);
- match Library::open_path(&path) {
- Err(Error::EntryTableOutOfBounds { .. }) => {}
- other => panic!("expected EntryTableOutOfBounds, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut unknown_method = build_rsli_bytes(
- &[SyntheticRsliEntry {
- name: "UNK".to_string(),
- method_raw: 0x120,
- plain: b"x".to_vec(),
- declared_packed_size: None,
- }],
- &RsliBuildOptions::default(),
- );
- // Force truly unknown method by writing 0x1C0 mask bits.
- let row = 32;
- unknown_method[row + 16..row + 18].copy_from_slice(&(0x1C0_u16).to_le_bytes());
- // Re-encrypt table with the same seed.
- let seed = u32::from_le_bytes([
- unknown_method[20],
- unknown_method[21],
- unknown_method[22],
- unknown_method[23],
- ]);
- let mut plain_row = vec![0u8; 32];
- plain_row.copy_from_slice(&unknown_method[32..64]);
- plain_row = xor_stream(&plain_row, (seed & 0xFFFF) as u16);
- plain_row[16..18].copy_from_slice(&(0x1C0_u16).to_le_bytes());
- let encrypted_row = xor_stream(&plain_row, (seed & 0xFFFF) as u16);
- unknown_method[32..64].copy_from_slice(&encrypted_row);
-
- let path = write_temp_file("rsli-unknown-method", &unknown_method);
- let lib = Library::open_path(&path).expect("open archive with unknown method failed");
- match lib.load(EntryId(0)) {
- Err(Error::UnsupportedMethod { raw }) => assert_eq!(raw, 0x1C0),
- other => panic!("expected UnsupportedMethod, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut bad_packed = valid.clone();
- bad_packed[32 + 28..32 + 32].copy_from_slice(&0xFFFF_FFF0_u32.to_le_bytes());
- let path = write_temp_file("rsli-bad-packed", &bad_packed);
- match Library::open_path(&path) {
- Err(Error::PackedSizePastEof { .. }) => {}
- other => panic!("expected PackedSizePastEof, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-
- let mut with_bad_overlay = valid;
- with_bad_overlay.extend_from_slice(b"AO");
- with_bad_overlay.extend_from_slice(&0xFFFF_FFFF_u32.to_le_bytes());
- let path = write_temp_file("rsli-bad-overlay", &with_bad_overlay);
- match Library::open_path_with(
- &path,
- OpenOptions {
- allow_ao_trailer: true,
- allow_deflate_eof_plus_one: true,
- },
- ) {
- Err(Error::MediaOverlayOutOfBounds { .. }) => {}
- other => panic!("expected MediaOverlayOutOfBounds, got {other:?}"),
- }
- let _ = fs::remove_file(&path);
-}
-
-proptest! {
- #![proptest_config(ProptestConfig::with_cases(64))]
-
- #[test]
- fn parse_library_is_panic_free_on_random_bytes(data in proptest::collection::vec(any::<u8>(), 0..4096)) {
- let _ = crate::parse::parse_library(
- Arc::from(data.into_boxed_slice()),
- OpenOptions::default(),
- );
- }
-}