diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-12 00:43:40 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-12 00:43:40 +0300 |
| commit | 662b292b5b47d0f7df3b19808db746bbc2ecc48c (patch) | |
| tree | df80622739c975eda645acf8754f3331cbdd1c82 /crates/rsli | |
| parent | 3410b54793c3a1808e58d0fae94fb2ebd5f81015 (diff) | |
| download | fparkan-662b292b5b47d0f7df3b19808db746bbc2ecc48c.tar.xz fparkan-662b292b5b47d0f7df3b19808db746bbc2ecc48c.zip | |
feat: обновить методы обработки данных и улучшить обработку ошибок в библиотеке
Diffstat (limited to 'crates/rsli')
| -rw-r--r-- | crates/rsli/src/compress/deflate.rs | 13 | ||||
| -rw-r--r-- | crates/rsli/src/compress/lzh.rs | 30 | ||||
| -rw-r--r-- | crates/rsli/src/error.rs | 9 | ||||
| -rw-r--r-- | crates/rsli/src/lib.rs | 10 | ||||
| -rw-r--r-- | crates/rsli/src/tests.rs | 174 |
5 files changed, 206 insertions, 30 deletions
diff --git a/crates/rsli/src/compress/deflate.rs b/crates/rsli/src/compress/deflate.rs index 154e0e3..6b8ea73 100644 --- a/crates/rsli/src/compress/deflate.rs +++ b/crates/rsli/src/compress/deflate.rs @@ -1,19 +1,14 @@ use crate::error::Error; use crate::Result; -use flate2::read::{DeflateDecoder, ZlibDecoder}; +use flate2::read::DeflateDecoder; use std::io::Read; -/// Decode Deflate or Zlib compressed data +/// 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); - if decoder.read_to_end(&mut out).is_ok() { - return Ok(out); - } - - out.clear(); - let mut zlib = ZlibDecoder::new(packed); - zlib.read_to_end(&mut out) + 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 index fa9cff7..07dc0c5 100644 --- a/crates/rsli/src/compress/lzh.rs +++ b/crates/rsli/src/compress/lzh.rs @@ -52,14 +52,14 @@ impl<'a> LzhDecoder<'a> { let mut out = Vec::with_capacity(expected_size); while out.len() < expected_size { - let c = self.decode_char(); + 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(); + 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); @@ -131,29 +131,29 @@ impl<'a> LzhDecoder<'a> { self.parent[LZH_R] = 0; } - fn decode_char(&mut self) -> usize { + 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_or_zero()); + let bit = usize::from(self.bit_reader.read_bit()?); node = self.son[node + bit]; } let c = node - LZH_T; self.update(c); - c + Ok(c) } - fn decode_position(&mut self) -> usize { - let i = self.bit_reader.read_bits_or_zero(8) as usize; + 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_or_zero()) << j; + c |= usize::from(self.bit_reader.read_bit()?) << j; } - c | (i & 0x3F) + Ok(c | (i & 0x3F)) } fn update(&mut self, c: usize) { @@ -264,10 +264,10 @@ impl<'a> BitReader<'a> { } } - fn read_bit_or_zero(&mut self) -> u8 { + 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 0; + return Err(Error::DecompressionFailed("lzss-huffman: unexpected EOF")); }; if let Some(state) = &mut self.xor_state { byte = state.decrypt_byte(byte); @@ -285,14 +285,14 @@ impl<'a> BitReader<'a> { self.bit_mask = 0x80; self.byte_pos = self.byte_pos.saturating_add(1); } - bit + Ok(bit) } - fn read_bits_or_zero(&mut self, bits: usize) -> u32 { + 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_or_zero()); + value = (value << 1) | u32::from(self.read_bit()?); } - value + Ok(value) } } diff --git a/crates/rsli/src/error.rs b/crates/rsli/src/error.rs index c2d5a08..5a36101 100644 --- a/crates/rsli/src/error.rs +++ b/crates/rsli/src/error.rs @@ -130,4 +130,11 @@ impl fmt::Display for Error { } } -impl std::error::Error for Error {} +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 index 1573898..ef29f41 100644 --- a/crates/rsli/src/lib.rs +++ b/crates/rsli/src/lib.rs @@ -191,7 +191,7 @@ impl Library { pub fn load(&self, id: EntryId) -> Result<Vec<u8>> { let entry = self.entry_by_id(id)?; - let packed = self.packed_slice(entry)?; + let packed = self.packed_slice(id, entry)?; decode_payload( packed, entry.meta.method, @@ -208,7 +208,7 @@ impl Library { pub fn load_packed(&self, id: EntryId) -> Result<PackedResource> { let entry = self.entry_by_id(id)?; - let packed = self.packed_slice(entry)?.to_vec(); + let packed = self.packed_slice(id, entry)?.to_vec(); Ok(PackedResource { meta: entry.meta.clone(), packed, @@ -231,7 +231,7 @@ impl Library { 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(entry)?; + let packed = self.packed_slice(id, entry)?; let size = usize::try_from(entry.meta.unpacked_size).map_err(|_| Error::IntegerOverflow)?; if packed.len() < size { @@ -255,7 +255,7 @@ impl Library { }) } - fn packed_slice<'a>(&'a self, entry: &EntryRecord) -> Result<&'a [u8]> { + 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) @@ -263,7 +263,7 @@ impl Library { self.bytes .get(start..end) .ok_or(Error::EntryDataOutOfBounds { - id: 0, + 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), diff --git a/crates/rsli/src/tests.rs b/crates/rsli/src/tests.rs index ade5c2c..33fc71b 100644 --- a/crates/rsli/src/tests.rs +++ b/crates/rsli/src/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T}; use crate::compress::xor::xor_stream; use flate2::write::DeflateEncoder; +use flate2::write::ZlibEncoder; use flate2::Compression; use std::any::Any; use std::fs; @@ -103,6 +104,12 @@ fn deflate_raw(data: &[u8]) -> Vec<u8> { 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) { @@ -796,6 +803,131 @@ fn rsli_synthetic_all_methods_roundtrip() { } #[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 { @@ -843,6 +975,48 @@ fn rsli_presorted_flag_requires_permutation() { } #[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