diff options
Diffstat (limited to 'crates/rsli/src')
| -rw-r--r-- | crates/rsli/src/compress/deflate.rs | 14 | ||||
| -rw-r--r-- | crates/rsli/src/compress/lzh.rs | 303 | ||||
| -rw-r--r-- | crates/rsli/src/compress/lzss.rs | 79 | ||||
| -rw-r--r-- | crates/rsli/src/compress/mod.rs | 9 | ||||
| -rw-r--r-- | crates/rsli/src/compress/xor.rs | 29 | ||||
| -rw-r--r-- | crates/rsli/src/error.rs | 140 | ||||
| -rw-r--r-- | crates/rsli/src/lib.rs | 470 | ||||
| -rw-r--r-- | crates/rsli/src/parse.rs | 278 | ||||
| -rw-r--r-- | crates/rsli/src/tests.rs | 1338 |
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(), - ); - } -} |
