use crate::block::decompress_block_payload;
use crate::config::EngineConfiguration;
use crate::format::{BlockHeader, BlockIndexEntry, FileFooter, IndexHeader};
use crush_core::error::{CrushError, Result};
use std::io::{Read, Seek, SeekFrom};
#[derive(Debug, Clone)]
pub struct BlockIndex {
pub entries: Vec<BlockIndexEntry>,
pub checksums_enabled: bool,
}
impl BlockIndex {
#[must_use]
pub fn uncompressed_offset(&self, block_n: u64) -> u64 {
let n = usize::try_from(block_n).unwrap_or(usize::MAX);
self.entries
.iter()
.take(n)
.map(|e| u64::from(e.uncompressed_size))
.sum()
}
#[must_use]
pub fn block_for_offset(&self, uncompressed_offset: u64) -> Option<u64> {
let mut cumulative: u64 = 0;
for (i, entry) in self.entries.iter().enumerate() {
let next = cumulative + u64::from(entry.uncompressed_size);
if uncompressed_offset < next {
return Some(i as u64);
}
cumulative = next;
}
None
}
#[must_use]
pub fn total_uncompressed_size(&self) -> u64 {
self.entries
.iter()
.map(|e| u64::from(e.uncompressed_size))
.sum()
}
#[must_use]
pub fn len(&self) -> u64 {
self.entries.len() as u64
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub fn load_index<R: Read + Seek>(reader: &mut R) -> Result<BlockIndex> {
let file_size = reader.seek(SeekFrom::End(0))?;
if file_size < FileFooter::SIZE as u64 {
return Err(CrushError::IndexCorrupted(format!(
"file too short ({file_size} bytes) to contain a CRSH footer"
)));
}
reader.seek(SeekFrom::Start(file_size - FileFooter::SIZE as u64))?;
let mut footer_buf = [0u8; FileFooter::SIZE];
reader.read_exact(&mut footer_buf)?;
let footer = FileFooter::from_bytes(&footer_buf)?;
let index_end = footer.index_offset + u64::from(footer.index_size);
if index_end > file_size - FileFooter::SIZE as u64 {
return Err(CrushError::IndexCorrupted(
"index region extends beyond footer position".to_owned(),
));
}
reader.seek(SeekFrom::Start(footer.index_offset))?;
let mut ih_buf = [0u8; IndexHeader::SIZE];
reader.read_exact(&mut ih_buf)?;
let ih = IndexHeader::from_bytes(&ih_buf);
let entry_count = ih.entry_count as usize;
let mut entries = Vec::with_capacity(entry_count);
for i in 0..entry_count {
let mut e_buf = [0u8; BlockIndexEntry::SIZE];
reader
.read_exact(&mut e_buf)
.map_err(|e| CrushError::IndexCorrupted(format!("truncated at entry {i}: {e}")))?;
entries.push(BlockIndexEntry::from_bytes(&e_buf));
}
let checksums_enabled = entries.first().is_some_and(|e| e.checksum != 0);
Ok(BlockIndex {
entries,
checksums_enabled,
})
}
pub fn decompress_block<R: Read + Seek>(
reader: &mut R,
block_index: &BlockIndex,
block_n: u64,
_config: &EngineConfiguration,
) -> Result<Vec<u8>> {
let block_n_usize = usize::try_from(block_n)
.map_err(|_| CrushError::InvalidConfig(format!("block_n {block_n} overflows usize")))?;
let entry = block_index.entries.get(block_n_usize).ok_or_else(|| {
CrushError::InvalidConfig(format!(
"block_n {block_n} out of range (index has {} entries)",
block_index.entries.len()
))
})?;
reader.seek(SeekFrom::Start(entry.block_offset))?;
let mut hdr_buf = [0u8; BlockHeader::SIZE];
reader.read_exact(&mut hdr_buf)?;
let header = BlockHeader::from_bytes(&hdr_buf);
let mut payload = vec![0u8; header.compressed_size as usize];
reader.read_exact(&mut payload)?;
decompress_block_payload(&header, &payload, block_n, block_index.checksums_enabled)
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::cast_possible_truncation
)]
mod tests {
use super::*;
use crate::config::EngineConfiguration;
use crate::engine::compress;
use std::io::Cursor;
fn make_test_data() -> Vec<u8> {
b"ABCDEFGH"
.iter()
.cycle()
.take(4 * 1_048_576)
.copied()
.collect()
}
#[test]
fn test_decompress_block_n() {
let data = make_test_data();
let config = EngineConfiguration::builder()
.block_size(1_048_576)
.build()
.expect("config");
let compressed = compress(&data, &config).expect("compress");
let mut cursor = Cursor::new(&compressed);
let index = load_index(&mut cursor).expect("load_index");
let last = index.len() - 1;
let recovered =
decompress_block(&mut cursor, &index, last, &config).expect("decompress_block");
let expected_offset = index.uncompressed_offset(last) as usize;
let expected_size = index.entries[last as usize].uncompressed_size as usize;
assert_eq!(
recovered,
&data[expected_offset..expected_offset + expected_size]
);
}
#[test]
fn test_block_for_offset() {
let data = make_test_data();
let config = EngineConfiguration::builder()
.block_size(1_048_576)
.build()
.expect("config");
let compressed = compress(&data, &config).expect("compress");
let mut cursor = Cursor::new(&compressed);
let index = load_index(&mut cursor).expect("load_index");
assert_eq!(index.block_for_offset(0), Some(0));
let block2_start = index.uncompressed_offset(2);
assert_eq!(index.block_for_offset(block2_start), Some(2));
assert_eq!(index.block_for_offset(data.len() as u64), None);
}
#[test]
fn test_random_access_does_not_read_other_blocks() {
let data = make_test_data();
let config = EngineConfiguration::builder()
.block_size(1_048_576)
.build()
.expect("config");
let compressed = compress(&data, &config).expect("compress");
let total = compressed.len();
let _ = total;
let mut cursor = Cursor::new(&compressed);
let index = load_index(&mut cursor).expect("load_index");
let _block0 = decompress_block(&mut cursor, &index, 0, &config).expect("block 0");
}
}