diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-10 11:57:00 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-10 11:57:00 +0300 |
| commit | ba1789f10607f5a6cba5863128d31f776b8e59cc (patch) | |
| tree | cc090228196bddc5a700c5ec32ec61f53c44a4b4 | |
| parent | 842f4a85693b418af81560738aa3136ac500d9b1 (diff) | |
| download | fparkan-ba1789f10607f5a6cba5863128d31f776b8e59cc.tar.xz fparkan-ba1789f10607f5a6cba5863128d31f776b8e59cc.zip | |
fix: обработка выхода за пределы индекса сортировки в архиве и улучшение декодирования LZSS с поддержкой XOR
| -rw-r--r-- | crates/nres/src/lib.rs | 9 | ||||
| -rw-r--r-- | crates/nres/src/tests.rs | 46 | ||||
| -rw-r--r-- | crates/rsli/src/compress/lzh.rs | 44 | ||||
| -rw-r--r-- | crates/rsli/src/tests.rs | 38 |
4 files changed, 116 insertions, 21 deletions
diff --git a/crates/nres/src/lib.rs b/crates/nres/src/lib.rs index 0cd9e22..1fa3b39 100644 --- a/crates/nres/src/lib.rs +++ b/crates/nres/src/lib.rs @@ -111,7 +111,9 @@ impl Archive { let mut high = self.entries.len(); while low < high { let mid = low + (high - low) / 2; - let target_idx = self.entries[mid].meta.sort_index as usize; + let Ok(target_idx) = usize::try_from(self.entries[mid].meta.sort_index) else { + break; + }; if target_idx >= self.entries.len() { break; } @@ -396,7 +398,10 @@ fn parse_archive(bytes: &[u8], raw_mode: bool) -> Result<(Vec<EntryRecord>, u64) name }, }; - return Ok((vec![entry], bytes.len() as u64)); + return Ok(( + vec![entry], + u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?, + )); } if bytes.len() < 16 { diff --git a/crates/nres/src/tests.rs b/crates/nres/src/tests.rs index 47aad53..43f155f 100644 --- a/crates/nres/src/tests.rs +++ b/crates/nres/src/tests.rs @@ -610,6 +610,52 @@ fn nres_synthetic_read_find_and_edit() { } #[test] +fn nres_find_falls_back_when_sort_index_is_out_of_range() { + let mut bytes = build_nres_bytes(&[ + SyntheticEntry { + kind: 1, + attr1: 0, + attr2: 0, + attr3: 0, + name: "Alpha", + data: b"a", + }, + SyntheticEntry { + kind: 2, + attr1: 0, + attr2: 0, + attr3: 0, + name: "Beta", + data: b"b", + }, + SyntheticEntry { + kind: 3, + attr1: 0, + attr2: 0, + attr3: 0, + name: "Gamma", + data: b"c", + }, + ]); + + let entry_count = 3usize; + let directory_offset = bytes + .len() + .checked_sub(entry_count * 64) + .expect("directory offset underflow"); + let mid_entry_sort_index = directory_offset + 64 + 60; + bytes[mid_entry_sort_index..mid_entry_sort_index + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + + let archive = Archive::open_bytes(Arc::from(bytes.into_boxed_slice()), OpenOptions::default()) + .expect("open archive with corrupted sort index failed"); + + assert_eq!(archive.find("alpha"), Some(EntryId(0))); + assert_eq!(archive.find("BETA"), Some(EntryId(1))); + assert_eq!(archive.find("gamma"), Some(EntryId(2))); + assert_eq!(archive.find("missing"), None); +} + +#[test] fn nres_validation_error_cases() { let valid = build_nres_bytes(&[SyntheticEntry { kind: 1, diff --git a/crates/rsli/src/compress/lzh.rs b/crates/rsli/src/compress/lzh.rs index 93f2267..fa9cff7 100644 --- a/crates/rsli/src/compress/lzh.rs +++ b/crates/rsli/src/compress/lzh.rs @@ -1,4 +1,4 @@ -use super::xor::xor_stream; +use super::xor::XorState; use crate::error::Error; use crate::Result; @@ -10,22 +10,14 @@ 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 XOR pre-decryption +/// 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>> { - // TODO: Full optimization for Huffman variant (rare in practice) - // For now, fallback to separate XOR step for Huffman - if let Some(key) = xor_key { - let decrypted = xor_stream(data, key); - let mut decoder = LzhDecoder::new(&decrypted); - decoder.decode(expected_size) - } else { - let mut decoder = LzhDecoder::new(data); - decoder.decode(expected_size) - } + let mut decoder = LzhDecoder::new(data, xor_key); + decoder.decode(expected_size) } struct LzhDecoder<'a> { @@ -40,9 +32,9 @@ struct LzhDecoder<'a> { } impl<'a> LzhDecoder<'a> { - fn new(data: &'a [u8]) -> Self { + fn new(data: &'a [u8], xor_key: Option<u16>) -> Self { let mut decoder = Self { - bit_reader: BitReader::new(data), + bit_reader: BitReader::new(data, xor_key), text: [0x20u8; LZH_N], freq: [0u16; LZH_T + 1], parent: [0usize; LZH_T + LZH_N_CHAR], @@ -257,23 +249,37 @@ 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]) -> Self { + 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_or_zero(&mut self) -> u8 { - let Some(byte) = self.data.get(self.byte_pos).copied() else { - return 0; - }; + if self.bit_mask == 0x80 { + let Some(mut byte) = self.data.get(self.byte_pos).copied() else { + return 0; + }; + if let Some(state) = &mut self.xor_state { + byte = state.decrypt_byte(byte); + } + self.current_byte = byte; + } - let bit = if (byte & self.bit_mask) != 0 { 1 } else { 0 }; + 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; diff --git a/crates/rsli/src/tests.rs b/crates/rsli/src/tests.rs index 7ed16b1..94d14af 100644 --- a/crates/rsli/src/tests.rs +++ b/crates/rsli/src/tests.rs @@ -668,6 +668,44 @@ fn rsli_synthetic_all_methods_roundtrip() { } #[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