aboutsummaryrefslogtreecommitdiff
path: root/crates/rsli
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-10 11:26:49 +0300
committerValentin Popov <valentin@popov.link>2026-02-10 11:26:49 +0300
commitce6e30f7272fd0c064ef52ac85cad1c0f05fd323 (patch)
treeb493ba02b81a8a4759f44560c1fd5951b84428e1 /crates/rsli
parent4af183ad74bfaafa0dc9db8116d361582debe536 (diff)
downloadfparkan-ce6e30f7272fd0c064ef52ac85cad1c0f05fd323.tar.xz
fparkan-ce6e30f7272fd0c064ef52ac85cad1c0f05fd323.zip
feat: добавить библиотеку common с ресурсами и буферами вывода; обновить зависимости в nres и rsli
Diffstat (limited to 'crates/rsli')
-rw-r--r--crates/rsli/Cargo.toml1
-rw-r--r--crates/rsli/src/data.rs41
-rw-r--r--crates/rsli/src/error.rs4
-rw-r--r--crates/rsli/src/lib.rs153
4 files changed, 117 insertions, 82 deletions
diff --git a/crates/rsli/Cargo.toml b/crates/rsli/Cargo.toml
index 6f89e0a..faad224 100644
--- a/crates/rsli/Cargo.toml
+++ b/crates/rsli/Cargo.toml
@@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+common = { path = "../common" }
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
diff --git a/crates/rsli/src/data.rs b/crates/rsli/src/data.rs
deleted file mode 100644
index daa5592..0000000
--- a/crates/rsli/src/data.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use std::io;
-
-#[derive(Clone, Debug)]
-pub enum ResourceData<'a> {
- Borrowed(&'a [u8]),
- Owned(Vec<u8>),
-}
-
-impl<'a> ResourceData<'a> {
- pub fn as_slice(&self) -> &[u8] {
- match self {
- Self::Borrowed(slice) => slice,
- Self::Owned(buf) => buf.as_slice(),
- }
- }
-
- pub fn into_owned(self) -> Vec<u8> {
- match self {
- Self::Borrowed(slice) => slice.to_vec(),
- Self::Owned(buf) => buf,
- }
- }
-}
-
-impl AsRef<[u8]> for ResourceData<'_> {
- fn as_ref(&self) -> &[u8] {
- self.as_slice()
- }
-}
-
-pub trait OutputBuffer {
- fn write_exact(&mut self, data: &[u8]) -> io::Result<()>;
-}
-
-impl OutputBuffer for Vec<u8> {
- fn write_exact(&mut self, data: &[u8]) -> io::Result<()> {
- self.clear();
- self.extend_from_slice(data);
- Ok(())
- }
-}
diff --git a/crates/rsli/src/error.rs b/crates/rsli/src/error.rs
index 056a13b..c2d5a08 100644
--- a/crates/rsli/src/error.rs
+++ b/crates/rsli/src/error.rs
@@ -14,6 +14,9 @@ pub enum Error {
InvalidEntryCount {
got: i16,
},
+ TooManyEntries {
+ got: usize,
+ },
EntryTableOutOfBounds {
table_offset: u64,
@@ -75,6 +78,7 @@ impl fmt::Display for Error {
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,
diff --git a/crates/rsli/src/lib.rs b/crates/rsli/src/lib.rs
index ae800dc..52b905a 100644
--- a/crates/rsli/src/lib.rs
+++ b/crates/rsli/src/lib.rs
@@ -1,8 +1,7 @@
-pub mod data;
pub mod error;
-use crate::data::{OutputBuffer, ResourceData};
use crate::error::Error;
+use common::{OutputBuffer, ResourceData};
use flate2::read::{DeflateDecoder, ZlibDecoder};
use std::cmp::Ordering;
use std::fs;
@@ -112,7 +111,7 @@ impl Library {
.iter()
.enumerate()
.map(|(idx, entry)| EntryRef {
- id: EntryId(idx as u32),
+ id: EntryId(u32::try_from(idx).expect("entry count validated at parse")),
meta: &entry.meta,
})
}
@@ -122,9 +121,24 @@ impl Library {
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();
- let query_bytes = query.as_bytes();
+ 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 {
@@ -142,13 +156,20 @@ impl Library {
match cmp {
Ordering::Less => high = mid,
Ordering::Greater => low = mid + 1,
- Ordering::Equal => return Some(EntryId(idx as u32)),
+ Ordering::Equal => {
+ return Some(EntryId(
+ u32::try_from(idx).expect("entry count validated at parse"),
+ ))
+ }
}
}
+ // 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 {
- Some(EntryId(idx as u32))
+ Some(EntryId(
+ u32::try_from(idx).expect("entry count validated at parse"),
+ ))
} else {
None
}
@@ -292,14 +313,18 @@ impl Library {
}
for (idx, entry) in self.entries.iter().enumerate() {
- let packed = self.load_packed(EntryId(idx as u32))?.packed;
+ let packed = self
+ .load_packed(EntryId(
+ u32::try_from(idx).expect("entry count validated at parse"),
+ ))?
+ .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: idx as u32,
+ id: u32::try_from(idx).expect("entry count validated at parse"),
offset: u64::from(entry.data_offset_raw),
packed_size: entry.packed_size_declared,
file_len: u64::try_from(out.len()).map_err(|_| Error::IntegerOverflow)?,
@@ -347,6 +372,11 @@ fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
}
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 xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
@@ -410,11 +440,13 @@ fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
.checked_sub(1)
.ok_or(Error::IntegerOverflow)?;
} else {
- return Err(Error::DeflateEofPlusOneQuirkRejected { id: idx as u32 });
+ return Err(Error::DeflateEofPlusOneQuirkRejected {
+ id: u32::try_from(idx).expect("entry count validated at parse"),
+ });
}
} else {
return Err(Error::PackedSizePastEof {
- id: idx as u32,
+ id: u32::try_from(idx).expect("entry count validated at parse"),
offset: effective_offset_u64,
packed_size: packed_size_declared,
file_len: file_len_u64,
@@ -427,7 +459,7 @@ fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
.ok_or(Error::IntegerOverflow)?;
if available_end > bytes.len() {
return Err(Error::EntryDataOutOfBounds {
- id: idx as u32,
+ id: u32::try_from(idx).expect("entry count validated at parse"),
offset: effective_offset_u64,
size: packed_size_declared,
file_len: file_len_u64,
@@ -563,15 +595,15 @@ fn decode_payload(
}
xor_stream(&packed[..expected], key16)
}
- PackMethod::Lzss => lzss_decompress_simple(packed, expected)?,
+ PackMethod::Lzss => lzss_decompress_simple(packed, expected, None)?,
PackMethod::XorLzss => {
- let decrypted = xor_stream(packed, key16);
- lzss_decompress_simple(&decrypted, expected)?
+ // 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)?,
+ PackMethod::LzssHuffman => lzss_huffman_decompress(packed, expected, None)?,
PackMethod::XorLzssHuffman => {
- let decrypted = xor_stream(packed, key16);
- lzss_huffman_decompress(&decrypted, expected)?
+ // 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 }),
@@ -601,20 +633,37 @@ fn decode_deflate(packed: &[u8]) -> Result<Vec<u8>> {
Ok(out)
}
-fn xor_stream(data: &[u8], key16: u16) -> Vec<u8> {
- let mut lo = (key16 & 0xFF) as u8;
- let mut hi = ((key16 >> 8) & 0xFF) as u8;
+struct XorState {
+ lo: u8,
+ hi: u8,
+}
+
+impl XorState {
+ fn new(key16: u16) -> Self {
+ Self {
+ lo: (key16 & 0xFF) as u8,
+ hi: ((key16 >> 8) & 0xFF) as u8,
+ }
+ }
- let mut out = Vec::with_capacity(data.len());
- for value in data {
- lo = hi ^ lo.wrapping_shl(1);
- out.push(value ^ lo);
- hi = lo ^ (hi >> 1);
+ 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
}
- out
}
-fn lzss_decompress_simple(data: &[u8], expected_size: usize) -> Result<Vec<u8>> {
+fn xor_stream(data: &[u8], key16: u16) -> Vec<u8> {
+ let mut state = XorState::new(key16);
+ data.iter().map(|&b| state.decrypt_byte(b)).collect()
+}
+
+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);
@@ -623,31 +672,41 @@ fn lzss_decompress_simple(data: &[u8], expected_size: usize) -> Result<Vec<u8>>
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 Some(byte) = data.get(in_pos).copied() else {
- break;
- };
+ 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 Some(byte) = data.get(in_pos).copied() else {
- break;
- };
+ 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 (Some(low), Some(high)) =
- (data.get(in_pos).copied(), data.get(in_pos + 1).copied())
- else {
- break;
- };
+ 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);
@@ -683,9 +742,21 @@ const LZH_T: usize = LZH_N_CHAR * 2 - 1;
const LZH_R: usize = LZH_T - 1;
const LZH_MAX_FREQ: u16 = 0x8000;
-fn lzss_huffman_decompress(data: &[u8], expected_size: usize) -> Result<Vec<u8>> {
- let mut decoder = LzhDecoder::new(data);
- decoder.decode(expected_size)
+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)
+ }
}
struct LzhDecoder<'a> {