1use crate::block::decompress_block_payload;
4use crate::config::EngineConfiguration;
5use crate::format::{BlockHeader, BlockIndexEntry, FileFooter, IndexHeader};
6use crush_core::error::{CrushError, Result};
7use libdeflater::Decompressor;
8use std::io::{Read, Seek, SeekFrom};
9
10#[derive(Debug, Clone)]
17pub struct BlockIndex {
18 pub entries: Vec<BlockIndexEntry>,
19 pub checksums_enabled: bool,
20 cumulative_uncompressed: Vec<u64>,
23}
24
25impl BlockIndex {
26 fn new(entries: Vec<BlockIndexEntry>, checksums_enabled: bool) -> Self {
28 let mut cumulative_uncompressed = Vec::with_capacity(entries.len() + 1);
29 cumulative_uncompressed.push(0u64);
30 let mut running: u64 = 0;
31 for e in &entries {
32 running = running.saturating_add(u64::from(e.uncompressed_size));
35 cumulative_uncompressed.push(running);
36 }
37 Self {
38 entries,
39 checksums_enabled,
40 cumulative_uncompressed,
41 }
42 }
43
44 #[must_use]
47 pub fn uncompressed_offset(&self, block_n: u64) -> u64 {
48 let n = usize::try_from(block_n).unwrap_or(usize::MAX);
49 self.cumulative_uncompressed
52 .get(n)
53 .copied()
54 .unwrap_or_else(|| *self.cumulative_uncompressed.last().unwrap_or(&0))
55 }
56
57 #[must_use]
62 pub fn block_for_offset(&self, uncompressed_offset: u64) -> Option<u64> {
63 let total = self.total_uncompressed_size();
64 if uncompressed_offset >= total {
65 return None;
66 }
67 let i = self
70 .cumulative_uncompressed
71 .partition_point(|&x| x <= uncompressed_offset);
72 i.checked_sub(1).map(|idx| idx as u64)
74 }
75
76 #[must_use]
78 pub fn total_uncompressed_size(&self) -> u64 {
79 *self.cumulative_uncompressed.last().unwrap_or(&0)
80 }
81
82 #[must_use]
84 pub fn len(&self) -> u64 {
85 self.entries.len() as u64
86 }
87
88 #[must_use]
90 pub fn is_empty(&self) -> bool {
91 self.entries.is_empty()
92 }
93}
94
95pub fn load_index<R: Read + Seek>(reader: &mut R) -> Result<BlockIndex> {
104 let file_size = reader.seek(SeekFrom::End(0))?;
106 if file_size < FileFooter::SIZE as u64 {
107 return Err(CrushError::IndexCorrupted(format!(
108 "file too short ({file_size} bytes) to contain a CRSH footer"
109 )));
110 }
111
112 reader.seek(SeekFrom::Start(file_size - FileFooter::SIZE as u64))?;
114 let mut footer_buf = [0u8; FileFooter::SIZE];
115 reader.read_exact(&mut footer_buf)?;
116 let footer = FileFooter::from_bytes(&footer_buf)?;
117
118 let index_end = footer.index_offset + u64::from(footer.index_size);
120 if index_end > file_size - FileFooter::SIZE as u64 {
121 return Err(CrushError::IndexCorrupted(
122 "index region extends beyond footer position".to_owned(),
123 ));
124 }
125
126 reader.seek(SeekFrom::Start(footer.index_offset))?;
128 let mut ih_buf = [0u8; IndexHeader::SIZE];
129 reader.read_exact(&mut ih_buf)?;
130 let ih = IndexHeader::from_bytes(&ih_buf);
131
132 let entry_count = ih.entry_count as usize;
134 let mut entries = Vec::with_capacity(entry_count);
135 for i in 0..entry_count {
136 let mut e_buf = [0u8; BlockIndexEntry::SIZE];
137 reader
138 .read_exact(&mut e_buf)
139 .map_err(|e| CrushError::IndexCorrupted(format!("truncated at entry {i}: {e}")))?;
140 entries.push(BlockIndexEntry::from_bytes(&e_buf));
141 }
142
143 let checksums_enabled = entries.first().is_some_and(|e| e.checksum != 0);
145
146 Ok(BlockIndex::new(entries, checksums_enabled))
147}
148
149pub fn decompress_block<R: Read + Seek>(
160 reader: &mut R,
161 block_index: &BlockIndex,
162 block_n: u64,
163 _config: &EngineConfiguration,
164) -> Result<Vec<u8>> {
165 let block_n_usize = usize::try_from(block_n)
166 .map_err(|_| CrushError::InvalidConfig(format!("block_n {block_n} overflows usize")))?;
167 let entry = block_index.entries.get(block_n_usize).ok_or_else(|| {
168 CrushError::InvalidConfig(format!(
169 "block_n {block_n} out of range (index has {} entries)",
170 block_index.entries.len()
171 ))
172 })?;
173
174 reader.seek(SeekFrom::Start(entry.block_offset))?;
176
177 let mut hdr_buf = [0u8; BlockHeader::SIZE];
179 reader.read_exact(&mut hdr_buf)?;
180 let header = BlockHeader::from_bytes(&hdr_buf);
181
182 let mut payload = vec![0u8; header.compressed_size as usize];
184 reader.read_exact(&mut payload)?;
185
186 let mut decompressor = Decompressor::new();
188 decompress_block_payload(
189 &mut decompressor,
190 &header,
191 &payload,
192 block_n,
193 block_index.checksums_enabled,
194 )
195}
196
197#[cfg(test)]
198#[allow(
199 clippy::expect_used,
200 clippy::unwrap_used,
201 clippy::cast_possible_truncation
202)]
203mod tests {
204 use super::*;
205 use crate::config::EngineConfiguration;
206 use crate::engine::compress;
207 use std::io::Cursor;
208
209 fn make_test_data() -> Vec<u8> {
210 b"ABCDEFGH"
212 .iter()
213 .cycle()
214 .take(4 * 1_048_576)
215 .copied()
216 .collect()
217 }
218
219 #[test]
220 fn test_decompress_block_n() {
221 let data = make_test_data();
222 let config = EngineConfiguration::builder()
223 .block_size(1_048_576)
224 .build()
225 .expect("config");
226 let compressed = compress(&data, &config).expect("compress");
227 let mut cursor = Cursor::new(&compressed);
228 let index = load_index(&mut cursor).expect("load_index");
229
230 let last = index.len() - 1;
232 let recovered =
233 decompress_block(&mut cursor, &index, last, &config).expect("decompress_block");
234 let expected_offset = index.uncompressed_offset(last) as usize;
235 let expected_size = index.entries[last as usize].uncompressed_size as usize;
236 assert_eq!(
237 recovered,
238 &data[expected_offset..expected_offset + expected_size]
239 );
240 }
241
242 #[test]
243 fn test_block_for_offset() {
244 let data = make_test_data();
245 let config = EngineConfiguration::builder()
246 .block_size(1_048_576)
247 .build()
248 .expect("config");
249 let compressed = compress(&data, &config).expect("compress");
250 let mut cursor = Cursor::new(&compressed);
251 let index = load_index(&mut cursor).expect("load_index");
252
253 assert_eq!(index.block_for_offset(0), Some(0));
255 let block2_start = index.uncompressed_offset(2);
257 assert_eq!(index.block_for_offset(block2_start), Some(2));
258 assert_eq!(index.block_for_offset(data.len() as u64), None);
260
261 let block1_start = index.uncompressed_offset(1);
264 assert_eq!(index.block_for_offset(block1_start - 1), Some(0));
265 for k in 0..index.len() {
267 let off = index.uncompressed_offset(k);
268 assert_eq!(
269 index.block_for_offset(off),
270 Some(k),
271 "expected block_for_offset({off}) == Some({k})"
272 );
273 }
274 let last_block = index.len() - 1;
276 assert_eq!(
277 index.block_for_offset(data.len() as u64 - 1),
278 Some(last_block)
279 );
280 assert_eq!(index.total_uncompressed_size(), data.len() as u64);
282 }
283
284 #[test]
285 fn test_random_access_does_not_read_other_blocks() {
286 let data = make_test_data();
287 let config = EngineConfiguration::builder()
288 .block_size(1_048_576)
289 .build()
290 .expect("config");
291 let compressed = compress(&data, &config).expect("compress");
292 let total = compressed.len();
293
294 let _ = total; let mut cursor = Cursor::new(&compressed);
300 let index = load_index(&mut cursor).expect("load_index");
301 let _block0 = decompress_block(&mut cursor, &index, 0, &config).expect("block 0");
303 }
306}