fix: обработка выхода за пределы индекса сортировки в архиве и улучшение декодирования LZSS с поддержкой XOR
All checks were successful
Test / Lint (push) Successful in 47s
Test / Test (push) Successful in 41s

This commit is contained in:
2026-02-10 08:57:00 +00:00
parent 842f4a8569
commit ba1789f106
4 changed files with 116 additions and 21 deletions

View File

@@ -111,7 +111,9 @@ impl Archive {
let mut high = self.entries.len(); let mut high = self.entries.len();
while low < high { while low < high {
let mid = low + (high - low) / 2; 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() { if target_idx >= self.entries.len() {
break; break;
} }
@@ -396,7 +398,10 @@ fn parse_archive(bytes: &[u8], raw_mode: bool) -> Result<(Vec<EntryRecord>, u64)
name 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 { if bytes.len() < 16 {

View File

@@ -609,6 +609,52 @@ fn nres_synthetic_read_find_and_edit() {
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
} }
#[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] #[test]
fn nres_validation_error_cases() { fn nres_validation_error_cases() {
let valid = build_nres_bytes(&[SyntheticEntry { let valid = build_nres_bytes(&[SyntheticEntry {

View File

@@ -1,4 +1,4 @@
use super::xor::xor_stream; use super::xor::XorState;
use crate::error::Error; use crate::error::Error;
use crate::Result; 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_R: usize = LZH_T - 1;
pub(crate) const LZH_MAX_FREQ: u16 = 0x8000; 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( pub fn lzss_huffman_decompress(
data: &[u8], data: &[u8],
expected_size: usize, expected_size: usize,
xor_key: Option<u16>, xor_key: Option<u16>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
// TODO: Full optimization for Huffman variant (rare in practice) let mut decoder = LzhDecoder::new(data, xor_key);
// 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) decoder.decode(expected_size)
} else {
let mut decoder = LzhDecoder::new(data);
decoder.decode(expected_size)
}
} }
struct LzhDecoder<'a> { struct LzhDecoder<'a> {
@@ -40,9 +32,9 @@ struct LzhDecoder<'a> {
} }
impl<'a> 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 { let mut decoder = Self {
bit_reader: BitReader::new(data), bit_reader: BitReader::new(data, xor_key),
text: [0x20u8; LZH_N], text: [0x20u8; LZH_N],
freq: [0u16; LZH_T + 1], freq: [0u16; LZH_T + 1],
parent: [0usize; LZH_T + LZH_N_CHAR], parent: [0usize; LZH_T + LZH_N_CHAR],
@@ -257,23 +249,37 @@ struct BitReader<'a> {
data: &'a [u8], data: &'a [u8],
byte_pos: usize, byte_pos: usize,
bit_mask: u8, bit_mask: u8,
current_byte: u8,
xor_state: Option<XorState>,
} }
impl<'a> BitReader<'a> { impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self { fn new(data: &'a [u8], xor_key: Option<u16>) -> Self {
Self { Self {
data, data,
byte_pos: 0, byte_pos: 0,
bit_mask: 0x80, bit_mask: 0x80,
current_byte: 0,
xor_state: xor_key.map(XorState::new),
} }
} }
fn read_bit_or_zero(&mut self) -> u8 { fn read_bit_or_zero(&mut self) -> u8 {
let Some(byte) = self.data.get(self.byte_pos).copied() else { if self.bit_mask == 0x80 {
let Some(mut byte) = self.data.get(self.byte_pos).copied() else {
return 0; 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; self.bit_mask >>= 1;
if self.bit_mask == 0 { if self.bit_mask == 0 {
self.bit_mask = 0x80; self.bit_mask = 0x80;

View File

@@ -667,6 +667,44 @@ fn rsli_synthetic_all_methods_roundtrip() {
let _ = fs::remove_file(&path); 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] #[test]
fn rsli_synthetic_overlay_and_ao_trailer() { fn rsli_synthetic_overlay_and_ao_trailer() {
let entries = vec![SyntheticRsliEntry { let entries = vec![SyntheticRsliEntry {