1use itertools::Itertools;
10use rayon::prelude::*;
11use std::ops::Range;
12use thiserror::Error;
13use zerocopy::byteorder::{LE, U16, U32, U64};
14use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned};
15
16mod compression_algorithm;
17pub use compression_algorithm::{
18 CompressionAlgorithm, Compressor, Decompressor, ThreadLocalCompressor, ThreadLocalDecompressor,
19};
20mod compression_info;
21pub use compression_info::{CompressionInfo, DataBuffer, StreamingDecompressor};
22
23#[derive(Copy, Clone, Eq, PartialEq)]
26pub struct ZstdError(pub usize);
27
28impl std::fmt::Display for ZstdError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 let msg = zstd::zstd_safe::get_error_name(self.0);
31 let enum_code = unsafe { zstd::zstd_safe::zstd_sys::ZSTD_getErrorCode(self.0) };
32 write!(f, "{:?} ({})", enum_code, msg)
33 }
34}
35
36impl std::fmt::Debug for ZstdError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 std::fmt::Display::fmt(self, f)
39 }
40}
41
42#[cfg(target_os = "fuchsia")]
43impl From<ZstdError> for zx::Status {
44 fn from(err: ZstdError) -> Self {
45 use zstd::zstd_safe::zstd_sys::ZSTD_ErrorCode::*;
46 let code = unsafe { zstd::zstd_safe::zstd_sys::ZSTD_getErrorCode(err.0) };
47 match code {
48 ZSTD_error_corruption_detected
49 | ZSTD_error_checksum_wrong
50 | ZSTD_error_literals_headerWrong
51 | ZSTD_error_dictionary_corrupted
52 | ZSTD_error_prefix_unknown => zx::Status::IO_DATA_INTEGRITY,
53
54 ZSTD_error_version_unsupported
55 | ZSTD_error_frameParameter_unsupported
56 | ZSTD_error_parameter_unsupported => zx::Status::NOT_SUPPORTED,
57
58 ZSTD_error_parameter_outOfBound
59 | ZSTD_error_srcSize_wrong
60 | ZSTD_error_dstSize_tooSmall => zx::Status::INVALID_ARGS,
61
62 ZSTD_error_no_error
63 | ZSTD_error_GENERIC
64 | ZSTD_error_frameParameter_windowTooLarge
65 | ZSTD_error_dictionary_wrong
66 | ZSTD_error_dictionaryCreation_failed
67 | ZSTD_error_parameter_combination_unsupported
68 | ZSTD_error_tableLog_tooLarge
69 | ZSTD_error_maxSymbolValue_tooLarge
70 | ZSTD_error_maxSymbolValue_tooSmall
71 | ZSTD_error_stabilityCondition_notRespected
72 | ZSTD_error_stage_wrong
73 | ZSTD_error_init_missing
74 | ZSTD_error_memory_allocation
75 | ZSTD_error_workSpace_tooSmall
76 | ZSTD_error_dstBuffer_null
77 | ZSTD_error_noForwardProgress_destFull
78 | ZSTD_error_noForwardProgress_inputEmpty
79 | ZSTD_error_frameIndex_tooLarge
80 | ZSTD_error_seekableIO
81 | ZSTD_error_dstBuffer_wrong
82 | ZSTD_error_srcBuffer_wrong
83 | ZSTD_error_sequenceProducer_failed
84 | ZSTD_error_externalSequences_invalid
85 | ZSTD_error_cannotProduce_uncompressedBlock
86 | ZSTD_error_maxCode => zx::Status::INTERNAL,
87 }
88 }
89}
90
91#[derive(Debug, Error)]
92pub enum FormatError {
93 #[error("Zstd error: {0}")]
94 Zstd(ZstdError),
95 #[error("LZ4 error: {0}")]
96 Lz4(lz4::Error),
97}
98
99#[cfg(target_os = "fuchsia")]
100impl From<&FormatError> for zx::Status {
101 fn from(err: &FormatError) -> Self {
102 match err {
103 FormatError::Zstd(e) => zx::Status::from(*e),
104 FormatError::Lz4(_) => zx::Status::IO_DATA_INTEGRITY,
105 }
106 }
107}
108
109#[derive(Debug, Error)]
113pub enum ChunkedArchiveError {
114 #[error("Invalid or unsupported archive version.")]
115 InvalidVersion,
116
117 #[error("Archive header has incorrect magic.")]
118 BadMagic,
119
120 #[error("Integrity checks failed (e.g. incorrect CRC, inconsistent header fields).")]
121 IntegrityError,
122
123 #[error("Value is out of range or cannot be represented in specified type.")]
124 OutOfRange,
125
126 #[error("Error decompressing chunk {index}: {error}")]
127 DecompressionError { index: usize, error: FormatError },
128
129 #[error("Error compressing chunk {index}: {error}")]
130 CompressionError { index: usize, error: FormatError },
131}
132
133#[derive(Copy, Clone, Debug, Eq, PartialEq)]
135pub enum ChunkedArchiveOptions {
136 V2 {
138 minimum_chunk_size: usize,
143 chunk_alignment: usize,
145 compression_level: i32,
147 },
148 V3 {
150 compression_algorithm: CompressionAlgorithm,
152 },
153}
154
155impl ChunkedArchiveOptions {
156 const V2_VERSION: u16 = 2;
157 const V2_MAX_CHUNKS: usize = 1023;
158
159 const V3_VERSION: u16 = 3;
160 const V3_MAX_CHUNKS: usize = u32::MAX as usize;
161 const V3_CHUNK_SIZE: usize = 32 * 1024;
162 const V3_ZSTD_COMPRESSION_LEVEL: i32 = 22;
163
164 fn version(&self) -> u16 {
166 match self {
167 Self::V2 { .. } => Self::V2_VERSION,
168 Self::V3 { .. } => Self::V3_VERSION,
169 }
170 }
171
172 fn compression_algorithm(&self) -> CompressionAlgorithm {
174 match self {
175 Self::V2 { .. } => CompressionAlgorithm::Zstd,
176 Self::V3 { compression_algorithm } => *compression_algorithm,
177 }
178 }
179
180 fn chunk_size_for(&self, data_size: usize) -> usize {
182 match self {
183 Self::V2 { chunk_alignment, minimum_chunk_size: target_chunk_size, .. } => {
184 if data_size <= (Self::V2_MAX_CHUNKS * target_chunk_size) {
185 *target_chunk_size
186 } else {
187 let chunk_size = data_size.div_ceil(Self::V2_MAX_CHUNKS);
188 chunk_size.checked_next_multiple_of(*chunk_alignment).unwrap()
189 }
190 }
191 Self::V3 { .. } => {
192 assert!(
193 data_size.div_ceil(Self::V3_CHUNK_SIZE) <= Self::V3_MAX_CHUNKS,
194 "Chunked-compression V3 only supports data up to ~140TB"
195 );
196 Self::V3_CHUNK_SIZE
197 }
198 }
199 }
200
201 pub fn compressor(&self) -> Compressor {
203 match self {
204 Self::V2 { compression_level, .. } => {
205 let mut cctx = zstd::zstd_safe::CCtx::create();
206 cctx.set_parameter(zstd::zstd_safe::CParameter::CompressionLevel(
207 *compression_level,
208 ))
209 .expect("setting the compression level should never fail");
210 Compressor::Zstd(cctx)
211 }
212 Self::V3 { compression_algorithm: CompressionAlgorithm::Zstd } => {
213 let mut cctx = zstd::zstd_safe::CCtx::create();
214 cctx.set_parameter(zstd::zstd_safe::CParameter::CompressionLevel(
215 Self::V3_ZSTD_COMPRESSION_LEVEL,
216 ))
217 .expect("setting the compression level should never fail");
218 Compressor::Zstd(cctx)
219 }
220 Self::V3 { compression_algorithm: CompressionAlgorithm::Lz4 } => {
221 Compressor::Lz4 { compression_level: lz4::HcCompressionLevel::custom(12) }
222 }
223 }
224 }
225
226 pub fn thread_local_compressor(&self) -> ThreadLocalCompressor {
229 match self {
230 Self::V2 { compression_level, .. } => {
231 ThreadLocalCompressor::Zstd { compression_level: *compression_level }
232 }
233 Self::V3 { compression_algorithm: CompressionAlgorithm::Zstd } => {
234 ThreadLocalCompressor::Zstd { compression_level: Self::V3_ZSTD_COMPRESSION_LEVEL }
235 }
236 Self::V3 { compression_algorithm: CompressionAlgorithm::Lz4 } => {
237 ThreadLocalCompressor::Lz4 {
238 compression_level: lz4::HcCompressionLevel::custom(12),
239 }
240 }
241 }
242 }
243
244 fn is_valid_version(version: u16) -> bool {
246 match version {
247 Self::V2_VERSION => true,
248 Self::V3_VERSION => true,
249 _ => false,
250 }
251 }
252
253 fn max_chunks_for_version(version: u16) -> Result<usize, ChunkedArchiveError> {
256 match version {
257 Self::V2_VERSION => Ok(Self::V2_MAX_CHUNKS),
258 Self::V3_VERSION => Ok(Self::V3_MAX_CHUNKS),
259 _ => Err(ChunkedArchiveError::InvalidVersion),
260 }
261 }
262}
263
264#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct ChunkInfo {
268 pub decompressed_range: Range<usize>,
269 pub compressed_range: Range<usize>,
270}
271
272impl ChunkInfo {
273 fn from_entry(
274 entry: &SeekTableEntry,
275 header_length: usize,
276 ) -> Result<Self, ChunkedArchiveError> {
277 let decompressed_start = entry.decompressed_offset.get() as usize;
278 let decompressed_size = entry.decompressed_size.get() as usize;
279 let decompressed_range = decompressed_start
280 ..decompressed_start
281 .checked_add(decompressed_size)
282 .ok_or(ChunkedArchiveError::OutOfRange)?;
283
284 let compressed_offset = entry.compressed_offset.get() as usize;
285 let compressed_start = compressed_offset
286 .checked_sub(header_length)
287 .ok_or(ChunkedArchiveError::IntegrityError)?;
288 let compressed_size = entry.compressed_size.get() as usize;
289 let compressed_range = compressed_start
290 ..compressed_start
291 .checked_add(compressed_size)
292 .ok_or(ChunkedArchiveError::OutOfRange)?;
293
294 Ok(Self { decompressed_range, compressed_range })
295 }
296}
297
298#[derive(Debug)]
300pub struct DecodedArchive {
301 compression_algorithm: CompressionAlgorithm,
302 seek_table: Vec<ChunkInfo>,
303}
304
305impl DecodedArchive {
306 pub fn decompressed_size(&self) -> usize {
308 self.seek_table.last().map_or(0, |entry| entry.decompressed_range.end)
309 }
310
311 pub fn seek_table(&self) -> &[ChunkInfo] {
312 &self.seek_table
313 }
314}
315
316pub fn decode_archive(
320 data: &[u8],
321 archive_length: usize,
322) -> Result<Option<(DecodedArchive, &[u8])>, ChunkedArchiveError> {
323 match Ref::<_, ChunkedArchiveHeader>::from_prefix(data).map_err(Into::into) {
324 Ok((header, data)) => header.decode_archive(data, archive_length as u64),
325 Err(zerocopy::SizeError { .. }) => Ok(None), }
327}
328
329#[derive(IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned, Clone, Copy, Debug)]
331#[repr(C)]
332struct ChunkedArchiveHeader {
333 magic: [u8; 8],
334 version: U16<LE>,
335 compression_algorithm: u8,
339 reserved_0: u8,
340 num_entries: U32<LE>,
341 checksum: U32<LE>,
342 reserved_1: U32<LE>,
343 reserved_2: U64<LE>,
344}
345
346#[derive(IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned, Clone, Copy, Debug)]
348#[repr(C)]
349struct SeekTableEntry {
350 decompressed_offset: U64<LE>,
351 decompressed_size: U64<LE>,
352 compressed_offset: U64<LE>,
353 compressed_size: U64<LE>,
354}
355
356impl ChunkedArchiveHeader {
357 const CHUNKED_ARCHIVE_MAGIC: [u8; 8] = [0x46, 0x9b, 0x78, 0xef, 0x0f, 0xd0, 0xb2, 0x03];
358 const CHUNKED_ARCHIVE_CHECKSUM_OFFSET: usize = 16;
359
360 fn new(
361 seek_table: &[SeekTableEntry],
362 options: ChunkedArchiveOptions,
363 ) -> Result<Self, ChunkedArchiveError> {
364 let header: ChunkedArchiveHeader = Self {
365 magic: Self::CHUNKED_ARCHIVE_MAGIC,
366 version: options.version().into(),
367 compression_algorithm: options.compression_algorithm().into(),
368 reserved_0: 0.into(),
369 num_entries: TryInto::<u32>::try_into(seek_table.len())
370 .or(Err(ChunkedArchiveError::OutOfRange))?
371 .into(),
372 checksum: 0.into(), reserved_1: 0.into(),
374 reserved_2: 0.into(),
375 };
376 Ok(Self { checksum: header.checksum(seek_table).into(), ..header })
377 }
378
379 fn checksum(&self, entries: &[SeekTableEntry]) -> u32 {
381 let mut digest = crate::CRC_32.digest();
382 digest.update(&self.as_bytes()[..Self::CHUNKED_ARCHIVE_CHECKSUM_OFFSET]);
383 digest.update(
384 &self.as_bytes()
385 [Self::CHUNKED_ARCHIVE_CHECKSUM_OFFSET + self.checksum.as_bytes().len()..],
386 );
387 digest.update(entries.as_bytes());
388 digest.finalize()
389 }
390
391 fn header_length(num_entries: usize) -> usize {
393 std::mem::size_of::<ChunkedArchiveHeader>()
394 + (std::mem::size_of::<SeekTableEntry>() * num_entries)
395 }
396
397 fn decode_archive(
399 self,
400 data: &[u8],
401 archive_length: u64,
402 ) -> Result<Option<(DecodedArchive, &[u8])>, ChunkedArchiveError> {
403 let num_entries = self.num_entries.get() as usize;
405 let Ok((entries, chunk_data)) =
406 Ref::<_, [SeekTableEntry]>::from_prefix_with_elems(data, num_entries)
407 else {
408 return Ok(None);
409 };
410 let entries: &[SeekTableEntry] = Ref::into_ref(entries);
411
412 if self.magic != Self::CHUNKED_ARCHIVE_MAGIC {
414 return Err(ChunkedArchiveError::BadMagic);
415 }
416 let version = self.version.get();
417 if !ChunkedArchiveOptions::is_valid_version(version) {
418 return Err(ChunkedArchiveError::InvalidVersion);
419 }
420 if self.checksum.get() != self.checksum(entries) {
421 return Err(ChunkedArchiveError::IntegrityError);
422 }
423 if entries.len() > ChunkedArchiveOptions::max_chunks_for_version(version)? {
424 return Err(ChunkedArchiveError::IntegrityError);
425 }
426 let compression_algorithm = CompressionAlgorithm::try_from(self.compression_algorithm)?;
427
428 if !entries.is_empty() && entries[0].decompressed_offset.get() != 0 {
432 return Err(ChunkedArchiveError::IntegrityError);
433 }
434
435 let header_length = Self::header_length(entries.len());
437 if entries.iter().any(|entry| entry.compressed_offset.get() < header_length as u64) {
438 return Err(ChunkedArchiveError::IntegrityError);
439 }
440
441 for (prev, curr) in entries.iter().tuple_windows() {
444 if (prev.decompressed_offset.get() + prev.decompressed_size.get())
445 != curr.decompressed_offset.get()
446 {
447 return Err(ChunkedArchiveError::IntegrityError);
448 }
449 }
450
451 for (prev, curr) in entries.iter().tuple_windows() {
454 if (prev.compressed_offset.get() + prev.compressed_size.get())
455 > curr.compressed_offset.get()
456 {
457 return Err(ChunkedArchiveError::IntegrityError);
458 }
459 }
460
461 for entry in entries.iter() {
463 if entry.decompressed_size.get() == 0 || entry.compressed_size.get() == 0 {
464 return Err(ChunkedArchiveError::IntegrityError);
465 }
466 }
467
468 for entry in entries.iter() {
470 let compressed_end = entry.compressed_offset.get() + entry.compressed_size.get();
471 if compressed_end > archive_length {
472 return Err(ChunkedArchiveError::IntegrityError);
473 }
474 }
475
476 let seek_table = entries
477 .iter()
478 .map(|entry| ChunkInfo::from_entry(entry, header_length))
479 .try_collect()?;
480 Ok(Some((DecodedArchive { seek_table, compression_algorithm }, chunk_data)))
481 }
482}
483
484pub struct CompressedChunk {
486 pub compressed_data: Vec<u8>,
488 pub decompressed_size: usize,
490}
491
492pub struct ChunkedArchive {
494 chunks: Vec<CompressedChunk>,
497 chunk_size: usize,
500 options: ChunkedArchiveOptions,
502}
503
504impl ChunkedArchive {
505 pub fn new(data: &[u8], options: ChunkedArchiveOptions) -> Result<Self, ChunkedArchiveError> {
509 let chunk_size = options.chunk_size_for(data.len());
510 let mut chunks: Vec<Result<CompressedChunk, ChunkedArchiveError>> = vec![];
511 let compressor = options.thread_local_compressor();
512 data.par_chunks(chunk_size)
513 .enumerate()
514 .map(|(index, chunk)| {
515 let compressed_data = compressor.compress(chunk, index)?;
516 Ok(CompressedChunk { compressed_data, decompressed_size: chunk.len() })
517 })
518 .collect_into_vec(&mut chunks);
519 let chunks: Vec<_> = chunks.into_iter().try_collect()?;
520 Ok(ChunkedArchive { chunks, chunk_size, options })
521 }
522
523 pub fn chunks(&self) -> &Vec<CompressedChunk> {
525 &self.chunks
526 }
527
528 pub fn chunk_size(&self) -> usize {
532 self.chunk_size
533 }
534
535 pub fn compressed_data_size(&self) -> usize {
537 self.chunks.iter().map(|chunk| chunk.compressed_data.len()).sum()
538 }
539
540 pub fn serialized_size(&self) -> usize {
542 ChunkedArchiveHeader::header_length(self.chunks.len()) + self.compressed_data_size()
543 }
544
545 pub fn write(self, mut writer: impl std::io::Write) -> Result<(), std::io::Error> {
547 let seek_table = self.make_seek_table();
548 let header = ChunkedArchiveHeader::new(&seek_table, self.options).unwrap();
549 writer.write_all(header.as_bytes())?;
550 writer.write_all(seek_table.as_slice().as_bytes())?;
551 for chunk in self.chunks {
552 writer.write_all(&chunk.compressed_data)?;
553 }
554 Ok(())
555 }
556
557 fn make_seek_table(&self) -> Vec<SeekTableEntry> {
559 let header_length = ChunkedArchiveHeader::header_length(self.chunks.len());
560 let mut seek_table = vec![];
561 seek_table.reserve(self.chunks.len());
562 let mut compressed_size: usize = 0;
563 let mut decompressed_offset: usize = 0;
564 for chunk in &self.chunks {
565 seek_table.push(SeekTableEntry {
566 decompressed_offset: (decompressed_offset as u64).into(),
567 decompressed_size: (chunk.decompressed_size as u64).into(),
568 compressed_offset: ((header_length + compressed_size) as u64).into(),
569 compressed_size: (chunk.compressed_data.len() as u64).into(),
570 });
571 compressed_size += chunk.compressed_data.len();
572 decompressed_offset += chunk.decompressed_size;
573 }
574 seek_table
575 }
576}
577
578pub struct ChunkedDecompressor {
593 seek_table: Vec<ChunkInfo>,
594 buffer: Vec<u8>,
595 data_written: usize,
596 curr_chunk: usize,
597 total_compressed_size: usize,
598 decompressor: Decompressor,
599 decompressed_buffer: Vec<u8>,
600 error_handler: Option<ErrorHandler>,
601}
602
603type ErrorHandler = Box<dyn Fn(usize, ChunkInfo, &[u8]) -> () + Send + 'static>;
604
605impl ChunkedDecompressor {
606 pub fn new(decoded_archive: DecodedArchive) -> Result<Self, ChunkedArchiveError> {
608 let DecodedArchive { compression_algorithm, seek_table } = decoded_archive;
609 let total_compressed_size =
610 seek_table.last().map_or(0, |last_chunk| last_chunk.compressed_range.end);
611 let decompressed_buffer =
612 vec![0u8; seek_table.first().map_or(0, |c| c.decompressed_range.len())];
613 Ok(Self {
614 seek_table,
615 buffer: vec![],
616 data_written: 0,
617 curr_chunk: 0,
618 total_compressed_size,
619 decompressor: compression_algorithm.decompressor(),
620 decompressed_buffer,
621 error_handler: None,
622 })
623 }
624
625 pub fn new_with_error_handler(
628 decoded_archive: DecodedArchive,
629 error_handler: ErrorHandler,
630 ) -> Result<Self, ChunkedArchiveError> {
631 Ok(Self { error_handler: Some(error_handler), ..Self::new(decoded_archive)? })
632 }
633
634 pub fn algorithm(&self) -> CompressionAlgorithm {
636 match &self.decompressor {
637 Decompressor::Zstd => CompressionAlgorithm::Zstd,
638 Decompressor::Lz4 => CompressionAlgorithm::Lz4,
639 }
640 }
641
642 pub fn seek_table(&self) -> &Vec<ChunkInfo> {
643 &self.seek_table
644 }
645
646 fn finish_chunk(
647 &mut self,
648 data: &[u8],
649 chunk_callback: &mut impl FnMut(&[u8]) -> (),
650 ) -> Result<(), ChunkedArchiveError> {
651 debug_assert_eq!(data.len(), self.seek_table[self.curr_chunk].compressed_range.len());
652 let chunk = &self.seek_table[self.curr_chunk];
653 let decompressed_size = self
654 .decompressor
655 .decompress_into(data, self.decompressed_buffer.as_mut_slice(), self.curr_chunk)
656 .inspect_err(|_| {
657 if let Some(error_handler) = &self.error_handler {
658 error_handler(self.curr_chunk, chunk.clone(), data.as_bytes());
659 }
660 })?;
661 if decompressed_size != chunk.decompressed_range.len() {
662 return Err(ChunkedArchiveError::IntegrityError);
663 }
664 chunk_callback(&self.decompressed_buffer[..decompressed_size]);
665 self.curr_chunk += 1;
666 Ok(())
667 }
668
669 pub fn update(
671 &mut self,
672 mut data: &[u8],
673 chunk_callback: &mut impl FnMut(&[u8]) -> (),
674 ) -> Result<(), ChunkedArchiveError> {
675 if self.data_written + data.len() > self.total_compressed_size {
677 return Err(ChunkedArchiveError::OutOfRange);
678 }
679 self.data_written += data.len();
680
681 if !self.buffer.is_empty() {
683 let to_read = std::cmp::min(
684 data.len(),
685 self.seek_table[self.curr_chunk]
686 .compressed_range
687 .len()
688 .checked_sub(self.buffer.len())
689 .unwrap(),
690 );
691 self.buffer.extend_from_slice(&data[..to_read]);
692 if self.buffer.len() == self.seek_table[self.curr_chunk].compressed_range.len() {
693 let full_chunk = std::mem::take(&mut self.buffer);
697 self.finish_chunk(&full_chunk[..], chunk_callback)?;
698 self.buffer = full_chunk;
699 self.buffer.clear();
701 }
702 data = &data[to_read..];
703 }
704
705 while !data.is_empty()
707 && self.curr_chunk < self.seek_table.len()
708 && self.seek_table[self.curr_chunk].compressed_range.len() <= data.len()
709 {
710 let len = self.seek_table[self.curr_chunk].compressed_range.len();
711 self.finish_chunk(&data[..len], chunk_callback)?;
712 data = &data[len..];
713 }
714
715 if !data.is_empty() {
717 debug_assert!(self.curr_chunk < self.seek_table.len());
718 debug_assert!(self.data_written < self.total_compressed_size);
719 self.buffer.extend_from_slice(data);
720 }
721
722 debug_assert!(
723 self.data_written < self.total_compressed_size
724 || self.curr_chunk == self.seek_table.len()
725 );
726
727 Ok(())
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use crate::Type1Blob;
734
735 use super::*;
736 use rand::RngExt as _;
737 use std::matches;
738
739 #[test]
742 fn compress_simple() {
743 let data: Vec<u8> = vec![0; 32 * 1024 * 16];
744 let archive = ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS).unwrap();
745 let mut compressed: Vec<u8> = vec![];
747 archive.write(&mut compressed).unwrap();
748 assert!(compressed.len() <= data.len());
749 assert!(decode_archive(&compressed, compressed.len()).unwrap().is_some());
751 }
752
753 fn generate_archive(
755 num_entries: usize,
756 options: ChunkedArchiveOptions,
757 ) -> (ChunkedArchiveHeader, Vec<SeekTableEntry>, u64) {
758 let mut seek_table = Vec::with_capacity(num_entries);
759 let header_length = ChunkedArchiveHeader::header_length(num_entries) as u64;
760 const COMPRESSED_CHUNK_SIZE: u64 = 1024;
761 const DECOMPRESSED_CHUNK_SIZE: u64 = 2048;
762 for n in 0..(num_entries as u64) {
763 seek_table.push(SeekTableEntry {
764 compressed_offset: (header_length + (n * COMPRESSED_CHUNK_SIZE)).into(),
765 compressed_size: COMPRESSED_CHUNK_SIZE.into(),
766 decompressed_offset: (n * DECOMPRESSED_CHUNK_SIZE).into(),
767 decompressed_size: DECOMPRESSED_CHUNK_SIZE.into(),
768 });
769 }
770 let header = ChunkedArchiveHeader::new(&seek_table, options).unwrap();
771 let archive_length: u64 = header_length + (num_entries as u64 * COMPRESSED_CHUNK_SIZE);
772 (header, seek_table, archive_length)
773 }
774
775 #[test]
776 fn should_validate_self() {
777 let (header, seek_table, archive_length) =
778 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
779 let serialized_table = seek_table.as_slice().as_bytes();
780 assert!(header.decode_archive(serialized_table, archive_length).unwrap().is_some());
781 }
782
783 #[test]
784 fn should_validate_empty() {
785 let (header, _, archive_length) = generate_archive(0, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
786 assert!(header.decode_archive(&[], archive_length).unwrap().is_some());
787 }
788
789 #[test]
790 fn should_detect_bad_magic() {
791 let (header, seek_table, archive_length) =
792 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
793 let mut corrupt_magic = ChunkedArchiveHeader::CHUNKED_ARCHIVE_MAGIC;
794 corrupt_magic[0] = !corrupt_magic[0];
795 let bad_magic = ChunkedArchiveHeader { magic: corrupt_magic, ..header };
796 let serialized_table = seek_table.as_slice().as_bytes();
797 assert!(matches!(
798 bad_magic.decode_archive(serialized_table, archive_length).unwrap_err(),
799 ChunkedArchiveError::BadMagic
800 ));
801 }
802 #[test]
803 fn should_detect_wrong_version() {
804 let (header, seek_table, archive_length) =
805 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
806 let invalid_version = ChunkedArchiveHeader { version: u16::MAX.into(), ..header };
807 let serialized_table = seek_table.as_slice().as_bytes();
808 assert!(matches!(
809 invalid_version.decode_archive(serialized_table, archive_length).unwrap_err(),
810 ChunkedArchiveError::InvalidVersion
811 ));
812 }
813
814 #[test]
815 fn should_detect_corrupt_checksum() {
816 let (header, seek_table, archive_length) =
817 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
818 let corrupt_checksum =
819 ChunkedArchiveHeader { checksum: (!header.checksum.get()).into(), ..header };
820 let serialized_table = seek_table.as_slice().as_bytes();
821 assert!(matches!(
822 corrupt_checksum.decode_archive(serialized_table, archive_length).unwrap_err(),
823 ChunkedArchiveError::IntegrityError
824 ));
825 }
826
827 #[test]
828 fn should_reject_too_many_entries_v2() {
829 let (too_many_entries, seek_table, archive_length) = generate_archive(
830 ChunkedArchiveOptions::V2_MAX_CHUNKS + 1,
831 Type1Blob::CHUNKED_ARCHIVE_OPTIONS,
832 );
833
834 let serialized_table = seek_table.as_slice().as_bytes();
835 assert!(matches!(
836 too_many_entries.decode_archive(serialized_table, archive_length).unwrap_err(),
837 ChunkedArchiveError::IntegrityError
838 ));
839 }
840
841 #[test]
842 fn invariant_i0_first_entry_zero() {
843 let (header, mut seek_table, archive_length) =
844 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
845 assert_eq!(seek_table[0].decompressed_offset.get(), 0);
846 seek_table[0].decompressed_offset = 1.into();
847
848 let serialized_table = seek_table.as_slice().as_bytes();
849 assert!(matches!(
850 header.decode_archive(serialized_table, archive_length).unwrap_err(),
851 ChunkedArchiveError::IntegrityError
852 ));
853 }
854
855 #[test]
856 fn invariant_i1_no_header_overlap() {
857 let (header, mut seek_table, archive_length) =
858 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
859 let header_end = ChunkedArchiveHeader::header_length(seek_table.len()) as u64;
860 assert!(seek_table[0].compressed_offset.get() >= header_end);
861 seek_table[0].compressed_offset = (header_end - 1).into();
862 let serialized_table = seek_table.as_slice().as_bytes();
863 assert!(matches!(
864 header.decode_archive(serialized_table, archive_length).unwrap_err(),
865 ChunkedArchiveError::IntegrityError
866 ));
867 }
868
869 #[test]
870 fn invariant_i2_decompressed_monotonic() {
871 let (header, mut seek_table, archive_length) =
872 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
873 assert_eq!(
874 seek_table[0].decompressed_offset.get() + seek_table[0].decompressed_size.get(),
875 seek_table[1].decompressed_offset.get()
876 );
877 seek_table[1].decompressed_offset = (seek_table[1].decompressed_offset.get() - 1).into();
878 let serialized_table = seek_table.as_slice().as_bytes();
879 assert!(matches!(
880 header.decode_archive(serialized_table, archive_length).unwrap_err(),
881 ChunkedArchiveError::IntegrityError
882 ));
883 }
884
885 #[test]
886 fn invariant_i3_compressed_monotonic() {
887 let (header, mut seek_table, archive_length) =
888 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
889 assert!(
890 (seek_table[0].compressed_offset.get() + seek_table[0].compressed_size.get())
891 <= seek_table[1].compressed_offset.get()
892 );
893 seek_table[1].compressed_offset = (seek_table[1].compressed_offset.get() - 1).into();
894 let serialized_table = seek_table.as_slice().as_bytes();
895 assert!(matches!(
896 header.decode_archive(serialized_table, archive_length).unwrap_err(),
897 ChunkedArchiveError::IntegrityError
898 ));
899 }
900
901 #[test]
902 fn invariant_i4_nonzero_compressed_size() {
903 let (header, mut seek_table, archive_length) =
904 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
905 assert!(seek_table[0].compressed_size.get() > 0);
906 seek_table[0].compressed_size = 0.into();
907 let serialized_table = seek_table.as_slice().as_bytes();
908 assert!(matches!(
909 header.decode_archive(serialized_table, archive_length).unwrap_err(),
910 ChunkedArchiveError::IntegrityError
911 ));
912 }
913
914 #[test]
915 fn invariant_i4_nonzero_decompressed_size() {
916 let (header, mut seek_table, archive_length) =
917 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
918 assert!(seek_table[0].decompressed_size.get() > 0);
919 seek_table[0].decompressed_size = 0.into();
920 let serialized_table = seek_table.as_slice().as_bytes();
921 assert!(matches!(
922 header.decode_archive(serialized_table, archive_length).unwrap_err(),
923 ChunkedArchiveError::IntegrityError
924 ));
925 }
926
927 #[test]
928 fn invariant_i5_within_archive() {
929 let (header, mut seek_table, archive_length) =
930 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
931 let last_entry = seek_table.last_mut().unwrap();
932 assert!(
933 (last_entry.compressed_offset.get() + last_entry.compressed_size.get())
934 <= archive_length
935 );
936 last_entry.compressed_offset = (archive_length + 1).into();
937 let serialized_table = seek_table.as_slice().as_bytes();
938 assert!(matches!(
939 header.decode_archive(serialized_table, archive_length).unwrap_err(),
940 ChunkedArchiveError::IntegrityError
941 ));
942 }
943
944 #[test]
945 fn max_chunks() {
946 let ChunkedArchiveOptions::V2 { minimum_chunk_size, chunk_alignment, .. } =
947 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
948 else {
949 panic!()
950 };
951 assert_eq!(
952 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
953 .chunk_size_for(minimum_chunk_size * ChunkedArchiveOptions::V2_MAX_CHUNKS),
954 minimum_chunk_size
955 );
956 assert_eq!(
957 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
958 .chunk_size_for(minimum_chunk_size * ChunkedArchiveOptions::V2_MAX_CHUNKS + 1),
959 minimum_chunk_size + chunk_alignment
960 );
961 }
962
963 #[test]
964 fn test_decompressor_empty_archive() {
965 let mut compressed: Vec<u8> = vec![];
966 ChunkedArchive::new(&[], Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
967 .expect("compress")
968 .write(&mut compressed)
969 .expect("write archive");
970 let (decoded_archive, chunk_data) =
971 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
972 assert!(decoded_archive.seek_table.is_empty());
973 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
974 let mut chunk_callback = |_chunk: &[u8]| panic!("Archive doesn't have any chunks.");
975 chunk_data
977 .chunks(4)
978 .for_each(|data| decompressor.update(data, &mut chunk_callback).unwrap());
979 }
980
981 #[test]
982 fn test_decompressor() {
983 const UNCOMPRESSED_LENGTH: usize = 3_000_000;
984 let data: Vec<u8> = {
985 let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
986 rand::rng().sample_iter(&range).take(UNCOMPRESSED_LENGTH).collect()
987 };
988 let mut compressed: Vec<u8> = vec![];
989 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
990 .expect("compress")
991 .write(&mut compressed)
992 .expect("write archive");
993 let (decoded_archive, chunk_data) =
994 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
995
996 let num_chunks = decoded_archive.seek_table.len();
998 assert!(num_chunks > 1);
999
1000 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1001
1002 let mut decoded_chunks: usize = 0;
1003 let mut decompressed_offset: usize = 0;
1004 let mut chunk_callback = |decompressed_chunk: &[u8]| {
1005 assert!(
1006 decompressed_chunk
1007 == &data[decompressed_offset..decompressed_offset + decompressed_chunk.len()]
1008 );
1009 decompressed_offset += decompressed_chunk.len();
1010 decoded_chunks += 1;
1011 };
1012
1013 chunk_data
1015 .chunks(4)
1016 .for_each(|data| decompressor.update(data, &mut chunk_callback).unwrap());
1017 assert_eq!(decoded_chunks, num_chunks);
1018 }
1019
1020 #[test]
1021 fn test_decompressor_corrupt_decompressed_size() {
1022 let data = vec![0; 3_000_000];
1023 let mut compressed: Vec<u8> = vec![];
1024 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
1025 .expect("compress")
1026 .write(&mut compressed)
1027 .expect("write archive");
1028 let (mut decoded_archive, chunk_data) =
1029 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1030
1031 decoded_archive.seek_table[0].decompressed_range =
1033 decoded_archive.seek_table[0].decompressed_range.start
1034 ..decoded_archive.seek_table[0].decompressed_range.end + 1;
1035
1036 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1037 assert!(matches!(
1038 decompressor.update(&chunk_data, &mut |_chunk| {}),
1039 Err(ChunkedArchiveError::IntegrityError)
1040 ));
1041 }
1042
1043 #[test]
1044 fn test_decompressor_corrupt_compressed_size() {
1045 let data = vec![0; 3_000_000];
1046 let mut compressed: Vec<u8> = vec![];
1047 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
1048 .expect("compress")
1049 .write(&mut compressed)
1050 .expect("write archive");
1051 let (mut decoded_archive, chunk_data) =
1052 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1053
1054 decoded_archive.seek_table[0].compressed_range =
1056 decoded_archive.seek_table[0].compressed_range.start
1057 ..decoded_archive.seek_table[0].compressed_range.end - 1;
1058 let first_chunk_info = decoded_archive.seek_table[0].clone();
1059 let error_handler = move |chunk_index: usize, chunk_info: ChunkInfo, chunk_data: &[u8]| {
1060 assert_eq!(chunk_index, 0);
1061 assert_eq!(chunk_info, first_chunk_info);
1062 assert_eq!(chunk_data.len(), chunk_info.compressed_range.len());
1063 };
1064
1065 let mut decompressor =
1066 ChunkedDecompressor::new_with_error_handler(decoded_archive, Box::new(error_handler))
1067 .unwrap();
1068 assert!(matches!(
1069 decompressor.update(&chunk_data, &mut |_chunk| {}),
1070 Err(ChunkedArchiveError::DecompressionError { .. })
1071 ));
1072 }
1073
1074 #[test]
1075 fn test_decompressor_zstd_data_corruption() {
1076 let data = vec![0; 3_000_000];
1077 let mut compressed: Vec<u8> = vec![];
1078 let archive = match ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS) {
1079 Ok(a) => a,
1080 Err(e) => {
1081 panic!("Failed to compress in test: {:?}", e);
1082 }
1083 };
1084 archive.write(&mut compressed).expect("write archive");
1085 let (decoded_archive, chunk_data) =
1086 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1087
1088 let mut corrupt_data = chunk_data.to_vec();
1089 if corrupt_data.len() > 100 {
1090 corrupt_data[100] = !corrupt_data[100];
1091 }
1092
1093 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1094 let result = decompressor.update(&corrupt_data, &mut |_chunk| {});
1095 assert!(matches!(result, Err(ChunkedArchiveError::DecompressionError { .. })));
1096 }
1097
1098 #[test]
1099 fn test_v3_zstd_roundtrip() {
1100 let data = vec![0; 3_000_000];
1101 let options =
1102 ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Zstd };
1103 let mut compressed = vec![];
1104 ChunkedArchive::new(&data, options)
1105 .expect("compress")
1106 .write(&mut compressed)
1107 .expect("write");
1108
1109 let (header, _) =
1111 Ref::<_, ChunkedArchiveHeader>::from_prefix(compressed.as_slice()).unwrap();
1112 assert_eq!(header.version.get(), 3);
1113 assert_eq!(header.compression_algorithm, CompressionAlgorithm::Zstd as u8);
1114
1115 let (decoded_archive, chunk_data) =
1116 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1117
1118 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1120 let mut decompressed: Vec<u8> = vec![];
1121 let mut chunk_callback = |chunk: &[u8]| decompressed.extend_from_slice(chunk);
1122 decompressor.update(chunk_data, &mut chunk_callback).unwrap();
1123
1124 assert_eq!(decompressed, data);
1125 }
1126
1127 #[test]
1128 fn test_v3_lz4_roundtrip() {
1129 let data = vec![0; 3_000_000];
1130 let options =
1131 ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Lz4 };
1132 let mut compressed = vec![];
1133 ChunkedArchive::new(&data, options)
1134 .expect("compress")
1135 .write(&mut compressed)
1136 .expect("write");
1137
1138 let (header, _) =
1140 Ref::<_, ChunkedArchiveHeader>::from_prefix(compressed.as_slice()).unwrap();
1141 assert_eq!(header.version.get(), 3);
1142 assert_eq!(header.compression_algorithm, CompressionAlgorithm::Lz4 as u8);
1143
1144 let (decoded_archive, chunk_data) =
1145 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1146
1147 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1149 let mut decompressed: Vec<u8> = vec![];
1150 let mut chunk_callback = |chunk: &[u8]| decompressed.extend_from_slice(chunk);
1151 decompressor.update(chunk_data, &mut chunk_callback).unwrap();
1152
1153 assert_eq!(decompressed, data);
1154 }
1155}