fix: обработка выхода за пределы индекса сортировки в архиве и улучшение декодирования LZSS с поддержкой XOR
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user