use std::{
fs::File,
io::{Read, Seek, SeekFrom},
};
use crate::{
error::{ChmError, Result},
format::{LzxcControlData, LzxcResetTable},
lzx::LzxState,
};
const CACHE_SLOTS: usize = 5;
struct BlockCache {
slots: [Option<(u64, Vec<u8>)>; CACHE_SLOTS],
}
impl BlockCache {
const fn new() -> Self {
Self { slots: [const { None }; CACHE_SLOTS] }
}
#[allow(clippy::cast_possible_truncation)]
const fn slot(block_idx: u64) -> usize {
(block_idx % CACHE_SLOTS as u64) as usize
}
fn get(&self, block_idx: u64) -> Option<&[u8]> {
match &self.slots[Self::slot(block_idx)] {
Some((idx, data)) if *idx == block_idx => Some(data),
_ => None,
}
}
fn contains(&self, block_idx: u64) -> bool {
self.get(block_idx).is_some()
}
fn insert(&mut self, block_idx: u64, data: Vec<u8>) {
self.slots[Self::slot(block_idx)] = Some((block_idx, data));
}
}
pub struct Decompressor {
cn_abs_start: u64,
block_offsets: Vec<u64>,
block_len: usize,
reset_blkcount: u64,
lzx: Box<LzxState>,
lzx_last_block: Option<u64>,
cache: BlockCache,
compressed: Vec<u8>,
}
impl Decompressor {
pub fn new(
file: &mut File,
data_offset: u64,
cn_unit_start: u64,
rt_unit_start: u64,
rt_entry_len: u64,
reset_table: &LzxcResetTable,
ctl: &LzxcControlData,
) -> Result<Self> {
let window_bits = u8::try_from(ctl.window_size.trailing_zeros()).map_err(|_| ChmError::BadLzxc)?;
let block_len = usize::try_from(reset_table.block_len).map_err(|_| ChmError::Overflow)?;
if reset_table.block_len > u64::from(ctl.window_size) {
return Err(ChmError::BadResetTable);
}
let reset_blkcount =
u64::from(ctl.reset_interval / (ctl.window_size / 2)).saturating_mul(u64::from(ctl.windows_per_reset));
debug_assert!(reset_blkcount > 0);
let cn_abs_start = data_offset + cn_unit_start;
let stream_end = cn_abs_start.checked_add(reset_table.compressed_len).ok_or(ChmError::Overflow)?;
if stream_end > file.metadata()?.len() {
return Err(ChmError::BadResetTable);
}
let block_offsets = read_block_offsets(file, data_offset + rt_unit_start, rt_entry_len, reset_table)?;
Ok(Self {
cn_abs_start,
block_offsets,
block_len,
reset_blkcount,
lzx: LzxState::new(window_bits)?,
lzx_last_block: None,
cache: BlockCache::new(),
compressed: Vec::new(),
})
}
const fn uncompressed_len(&self) -> u64 {
(self.block_offsets.len() as u64 - 1) * self.block_len as u64
}
fn decode_one(&mut self, file: &mut File, block: u64) -> Result<()> {
let idx = usize::try_from(block).map_err(|_| ChmError::Overflow)?;
let (Some(&start), Some(&end)) = (self.block_offsets.get(idx), self.block_offsets.get(idx + 1)) else {
return Err(ChmError::BadResetTable);
};
let clen = end.checked_sub(start).ok_or(ChmError::BadResetTable)?;
let clen = usize::try_from(clen).map_err(|_| ChmError::Overflow)?;
self.compressed.clear();
self.compressed.resize(clen, 0);
file.seek(SeekFrom::Start(self.cn_abs_start + start))?;
file.read_exact(&mut self.compressed)?;
let mut decompressed = vec![0u8; self.block_len];
self.lzx.decompress(&self.compressed, &mut decompressed)?;
self.cache.insert(block, decompressed);
self.lzx_last_block = Some(block);
Ok(())
}
fn decompress_block(&mut self, file: &mut File, block: u64) -> Result<()> {
let window_start = block - block % self.reset_blkcount;
let start = match self.lzx_last_block {
Some(last) if last >= window_start && last < block => last + 1,
_ => window_start,
};
for b in start..=block {
if b.is_multiple_of(self.reset_blkcount) {
self.lzx.reset();
}
self.decode_one(file, b)?;
}
Ok(())
}
pub fn read(&mut self, file: &mut File, start: u64, len: u64) -> Result<Vec<u8>> {
let end = start.checked_add(len).ok_or(ChmError::Overflow)?;
if end > self.uncompressed_len() {
return Err(ChmError::BadResetTable);
}
let block_len = self.block_len as u64;
let mut result = Vec::with_capacity(usize::try_from(len).map_err(|_| ChmError::Overflow)?);
let mut pos = start;
while pos < end {
let block = pos / block_len;
let offset = usize::try_from(pos % block_len).map_err(|_| ChmError::Overflow)?;
let remaining = usize::try_from(end - pos).unwrap_or(usize::MAX);
let avail = (self.block_len - offset).min(remaining);
if !self.cache.contains(block) {
self.decompress_block(file, block)?;
}
let data = self.cache.get(block).ok_or(ChmError::NoCompression)?;
result.extend_from_slice(&data[offset..offset + avail]);
pos += avail as u64;
}
Ok(result)
}
}
fn read_block_offsets(
file: &mut File,
rt_abs_start: u64,
rt_entry_len: u64,
reset_table: &LzxcResetTable,
) -> Result<Vec<u64>> {
let block_count = u64::from(reset_table.block_count);
let table_offset = u64::from(reset_table.table_offset);
let table_len = block_count.checked_mul(8).ok_or(ChmError::Overflow)?;
let table_end = table_offset.checked_add(table_len).ok_or(ChmError::Overflow)?;
if table_end > rt_entry_len {
return Err(ChmError::BadResetTable);
}
let count = usize::try_from(block_count).map_err(|_| ChmError::Overflow)?;
let mut raw = vec![0u8; usize::try_from(table_len).map_err(|_| ChmError::Overflow)?];
file.seek(SeekFrom::Start(rt_abs_start + table_offset))?;
file.read_exact(&mut raw)?;
let mut offsets = Vec::with_capacity(count + 1);
for chunk in raw.chunks_exact(8) {
offsets.push(u64::from_le_bytes(chunk.try_into().unwrap()));
}
offsets.push(reset_table.compressed_len);
if offsets.windows(2).any(|w| w[0] > w[1]) {
return Err(ChmError::BadResetTable);
}
Ok(offsets)
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_possible_truncation)]
use super::*;
#[test]
fn cache_maps_blocks_to_slots_by_modulus() {
let mut cache = BlockCache::new();
cache.insert(0, vec![1, 2, 3]);
assert_eq!(cache.get(0), Some(&[1u8, 2, 3][..]));
assert!(cache.contains(0));
assert!(!cache.contains(1));
}
#[test]
fn cache_entry_is_evicted_by_a_colliding_block() {
let mut cache = BlockCache::new();
cache.insert(1, vec![1]);
cache.insert(1 + CACHE_SLOTS as u64, vec![2]);
assert!(!cache.contains(1));
assert_eq!(cache.get(1 + CACHE_SLOTS as u64), Some(&[2u8][..]));
}
#[test]
fn cache_holds_distinct_slots_simultaneously() {
let mut cache = BlockCache::new();
for i in 0..CACHE_SLOTS as u64 {
cache.insert(i, vec![i as u8]);
}
for i in 0..CACHE_SLOTS as u64 {
assert_eq!(cache.get(i), Some(&[i as u8][..]));
}
}
}