aboutsummaryrefslogtreecommitdiff
path: root/crates/rsli/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/rsli/src/lib.rs')
-rw-r--r--crates/rsli/src/lib.rs411
1 files changed, 411 insertions, 0 deletions
diff --git a/crates/rsli/src/lib.rs b/crates/rsli/src/lib.rs
new file mode 100644
index 0000000..ef29f41
--- /dev/null
+++ b/crates/rsli/src/lib.rs
@@ -0,0 +1,411 @@
+pub mod compress;
+pub mod error;
+pub mod parse;
+
+use crate::compress::{
+ decode_deflate, lzss_decompress_simple, lzss_huffman_decompress, xor_stream,
+};
+use crate::error::Error;
+use crate::parse::{c_name_bytes, cmp_c_string, parse_library};
+use common::{OutputBuffer, ResourceData};
+use std::cmp::Ordering;
+use std::fs;
+use std::path::Path;
+use std::sync::Arc;
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+#[derive(Clone, Debug)]
+pub struct OpenOptions {
+ pub allow_ao_trailer: bool,
+ pub allow_deflate_eof_plus_one: bool,
+}
+
+impl Default for OpenOptions {
+ fn default() -> Self {
+ Self {
+ allow_ao_trailer: true,
+ allow_deflate_eof_plus_one: true,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Library {
+ bytes: Arc<[u8]>,
+ entries: Vec<EntryRecord>,
+ #[cfg(test)]
+ pub(crate) header_raw: [u8; 32],
+ #[cfg(test)]
+ pub(crate) table_plain_original: Vec<u8>,
+ #[cfg(test)]
+ pub(crate) xor_seed: u32,
+ #[cfg(test)]
+ pub(crate) source_size: usize,
+ #[cfg(test)]
+ pub(crate) trailer_raw: Option<[u8; 6]>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct EntryId(pub u32);
+
+#[derive(Clone, Debug)]
+pub struct EntryMeta {
+ pub name: String,
+ pub flags: i32,
+ pub method: PackMethod,
+ pub data_offset: u64,
+ pub packed_size: u32,
+ pub unpacked_size: u32,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum PackMethod {
+ None,
+ XorOnly,
+ Lzss,
+ XorLzss,
+ LzssHuffman,
+ XorLzssHuffman,
+ Deflate,
+ Unknown(u32),
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct EntryRef<'a> {
+ pub id: EntryId,
+ pub meta: &'a EntryMeta,
+}
+
+pub struct PackedResource {
+ pub meta: EntryMeta,
+ pub packed: Vec<u8>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct EntryRecord {
+ pub(crate) meta: EntryMeta,
+ pub(crate) name_raw: [u8; 12],
+ pub(crate) sort_to_original: i16,
+ pub(crate) key16: u16,
+ #[cfg(test)]
+ pub(crate) data_offset_raw: u32,
+ pub(crate) packed_size_declared: u32,
+ pub(crate) packed_size_available: usize,
+ pub(crate) effective_offset: usize,
+}
+
+impl Library {
+ pub fn open_path(path: impl AsRef<Path>) -> Result<Self> {
+ Self::open_path_with(path, OpenOptions::default())
+ }
+
+ pub fn open_path_with(path: impl AsRef<Path>, opts: OpenOptions) -> Result<Self> {
+ let bytes = fs::read(path.as_ref())?;
+ let arc: Arc<[u8]> = Arc::from(bytes.into_boxed_slice());
+ parse_library(arc, opts)
+ }
+
+ pub fn entry_count(&self) -> usize {
+ self.entries.len()
+ }
+
+ pub fn entries(&self) -> impl Iterator<Item = EntryRef<'_>> {
+ self.entries
+ .iter()
+ .enumerate()
+ .map(|(idx, entry)| EntryRef {
+ id: EntryId(u32::try_from(idx).expect("entry count validated at parse")),
+ meta: &entry.meta,
+ })
+ }
+
+ pub fn find(&self, name: &str) -> Option<EntryId> {
+ if self.entries.is_empty() {
+ 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();
+ 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 {
+ let mid = low + (high - low) / 2;
+ let idx = self.entries[mid].sort_to_original;
+ if idx < 0 {
+ break;
+ }
+ let idx = usize::try_from(idx).ok()?;
+ if idx >= self.entries.len() {
+ break;
+ }
+
+ let cmp = cmp_c_string(query_bytes, c_name_bytes(&self.entries[idx].name_raw));
+ match cmp {
+ Ordering::Less => high = mid,
+ Ordering::Greater => low = mid + 1,
+ 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(
+ u32::try_from(idx).expect("entry count validated at parse"),
+ ))
+ } else {
+ None
+ }
+ })
+ }
+
+ pub fn get(&self, id: EntryId) -> Option<EntryRef<'_>> {
+ let idx = usize::try_from(id.0).ok()?;
+ let entry = self.entries.get(idx)?;
+ Some(EntryRef {
+ id,
+ meta: &entry.meta,
+ })
+ }
+
+ pub fn load(&self, id: EntryId) -> Result<Vec<u8>> {
+ let entry = self.entry_by_id(id)?;
+ let packed = self.packed_slice(id, entry)?;
+ decode_payload(
+ packed,
+ entry.meta.method,
+ entry.key16,
+ entry.meta.unpacked_size,
+ )
+ }
+
+ pub fn load_into(&self, id: EntryId, out: &mut dyn OutputBuffer) -> Result<usize> {
+ let decoded = self.load(id)?;
+ out.write_exact(&decoded)?;
+ Ok(decoded.len())
+ }
+
+ pub fn load_packed(&self, id: EntryId) -> Result<PackedResource> {
+ let entry = self.entry_by_id(id)?;
+ let packed = self.packed_slice(id, entry)?.to_vec();
+ Ok(PackedResource {
+ meta: entry.meta.clone(),
+ packed,
+ })
+ }
+
+ pub fn unpack(&self, packed: &PackedResource) -> Result<Vec<u8>> {
+ let key16 = self.resolve_key_for_meta(&packed.meta).unwrap_or(0);
+
+ let method = packed.meta.method;
+ if needs_xor_key(method) && self.resolve_key_for_meta(&packed.meta).is_none() {
+ return Err(Error::CorruptEntryTable(
+ "cannot resolve XOR key for packed resource",
+ ));
+ }
+
+ decode_payload(&packed.packed, method, key16, packed.meta.unpacked_size)
+ }
+
+ 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(id, entry)?;
+ let size =
+ usize::try_from(entry.meta.unpacked_size).map_err(|_| Error::IntegerOverflow)?;
+ if packed.len() < size {
+ return Err(Error::OutputSizeMismatch {
+ expected: entry.meta.unpacked_size,
+ got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
+ });
+ }
+ return Ok(ResourceData::Borrowed(&packed[..size]));
+ }
+ Ok(ResourceData::Owned(self.load(id)?))
+ }
+
+ fn entry_by_id(&self, id: EntryId) -> Result<&EntryRecord> {
+ let idx = usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?;
+ self.entries
+ .get(idx)
+ .ok_or_else(|| Error::EntryIdOutOfRange {
+ id: id.0,
+ entry_count: self.entries.len().try_into().unwrap_or(u32::MAX),
+ })
+ }
+
+ 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)
+ .ok_or(Error::IntegerOverflow)?;
+ self.bytes
+ .get(start..end)
+ .ok_or(Error::EntryDataOutOfBounds {
+ 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),
+ })
+ }
+
+ fn resolve_key_for_meta(&self, meta: &EntryMeta) -> Option<u16> {
+ self.entries
+ .iter()
+ .find(|entry| {
+ entry.meta.name == meta.name
+ && entry.meta.flags == meta.flags
+ && entry.meta.data_offset == meta.data_offset
+ && entry.meta.packed_size == meta.packed_size
+ && entry.meta.unpacked_size == meta.unpacked_size
+ && entry.meta.method == meta.method
+ })
+ .map(|entry| entry.key16)
+ }
+
+ #[cfg(test)]
+ pub(crate) fn rebuild_from_parsed_metadata(&self) -> Result<Vec<u8>> {
+ let trailer_len = usize::from(self.trailer_raw.is_some()) * 6;
+ let pre_trailer_size = self
+ .source_size
+ .checked_sub(trailer_len)
+ .ok_or(Error::IntegerOverflow)?;
+
+ let count = self.entries.len();
+ let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
+ let table_end = 32usize
+ .checked_add(table_len)
+ .ok_or(Error::IntegerOverflow)?;
+ if pre_trailer_size < table_end {
+ return Err(Error::EntryTableOutOfBounds {
+ table_offset: 32,
+ table_len: u64::try_from(table_len).map_err(|_| Error::IntegerOverflow)?,
+ file_len: u64::try_from(pre_trailer_size).map_err(|_| Error::IntegerOverflow)?,
+ });
+ }
+
+ let mut out = vec![0u8; pre_trailer_size];
+ out[0..32].copy_from_slice(&self.header_raw);
+ let encrypted_table =
+ xor_stream(&self.table_plain_original, (self.xor_seed & 0xFFFF) as u16);
+ out[32..table_end].copy_from_slice(&encrypted_table);
+
+ let mut occupied = vec![false; pre_trailer_size];
+ for byte in occupied.iter_mut().take(table_end) {
+ *byte = true;
+ }
+
+ for (idx, entry) in self.entries.iter().enumerate() {
+ 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: 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)?,
+ });
+ }
+ if occupied[pos] && out[pos] != byte {
+ return Err(Error::CorruptEntryTable("packed payload overlap conflict"));
+ }
+ out[pos] = byte;
+ occupied[pos] = true;
+ }
+ }
+
+ if let Some(trailer) = self.trailer_raw {
+ out.extend_from_slice(&trailer);
+ }
+ Ok(out)
+ }
+}
+
+fn decode_payload(
+ packed: &[u8],
+ method: PackMethod,
+ key16: u16,
+ unpacked_size: u32,
+) -> Result<Vec<u8>> {
+ let expected = usize::try_from(unpacked_size).map_err(|_| Error::IntegerOverflow)?;
+
+ let out = match method {
+ PackMethod::None => {
+ if packed.len() < expected {
+ return Err(Error::OutputSizeMismatch {
+ expected: unpacked_size,
+ got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
+ });
+ }
+ packed[..expected].to_vec()
+ }
+ PackMethod::XorOnly => {
+ if packed.len() < expected {
+ return Err(Error::OutputSizeMismatch {
+ expected: unpacked_size,
+ got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
+ });
+ }
+ xor_stream(&packed[..expected], key16)
+ }
+ PackMethod::Lzss => lzss_decompress_simple(packed, expected, None)?,
+ PackMethod::XorLzss => {
+ // 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, None)?,
+ PackMethod::XorLzssHuffman => {
+ // 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 }),
+ };
+
+ if out.len() != expected {
+ return Err(Error::OutputSizeMismatch {
+ expected: unpacked_size,
+ got: u32::try_from(out.len()).unwrap_or(u32::MAX),
+ });
+ }
+
+ Ok(out)
+}
+
+fn needs_xor_key(method: PackMethod) -> bool {
+ matches!(
+ method,
+ PackMethod::XorOnly | PackMethod::XorLzss | PackMethod::XorLzssHuffman
+ )
+}
+
+#[cfg(test)]
+mod tests;