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 crc_algo = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
382 let mut digest = crc_algo.digest();
383 digest.update(&self.as_bytes()[..Self::CHUNKED_ARCHIVE_CHECKSUM_OFFSET]);
384 digest.update(
385 &self.as_bytes()
386 [Self::CHUNKED_ARCHIVE_CHECKSUM_OFFSET + self.checksum.as_bytes().len()..],
387 );
388 digest.update(entries.as_bytes());
389 digest.finalize()
390 }
391
392 fn header_length(num_entries: usize) -> usize {
394 std::mem::size_of::<ChunkedArchiveHeader>()
395 + (std::mem::size_of::<SeekTableEntry>() * num_entries)
396 }
397
398 fn decode_archive(
400 self,
401 data: &[u8],
402 archive_length: u64,
403 ) -> Result<Option<(DecodedArchive, &[u8])>, ChunkedArchiveError> {
404 let num_entries = self.num_entries.get() as usize;
406 let Ok((entries, chunk_data)) =
407 Ref::<_, [SeekTableEntry]>::from_prefix_with_elems(data, num_entries)
408 else {
409 return Ok(None);
410 };
411 let entries: &[SeekTableEntry] = Ref::into_ref(entries);
412
413 if self.magic != Self::CHUNKED_ARCHIVE_MAGIC {
415 return Err(ChunkedArchiveError::BadMagic);
416 }
417 let version = self.version.get();
418 if !ChunkedArchiveOptions::is_valid_version(version) {
419 return Err(ChunkedArchiveError::InvalidVersion);
420 }
421 if self.checksum.get() != self.checksum(entries) {
422 return Err(ChunkedArchiveError::IntegrityError);
423 }
424 if entries.len() > ChunkedArchiveOptions::max_chunks_for_version(version)? {
425 return Err(ChunkedArchiveError::IntegrityError);
426 }
427 let compression_algorithm = CompressionAlgorithm::try_from(self.compression_algorithm)?;
428
429 if !entries.is_empty() && entries[0].decompressed_offset.get() != 0 {
433 return Err(ChunkedArchiveError::IntegrityError);
434 }
435
436 let header_length = Self::header_length(entries.len());
438 if entries.iter().any(|entry| entry.compressed_offset.get() < header_length as u64) {
439 return Err(ChunkedArchiveError::IntegrityError);
440 }
441
442 for (prev, curr) in entries.iter().tuple_windows() {
445 if (prev.decompressed_offset.get() + prev.decompressed_size.get())
446 != curr.decompressed_offset.get()
447 {
448 return Err(ChunkedArchiveError::IntegrityError);
449 }
450 }
451
452 for (prev, curr) in entries.iter().tuple_windows() {
455 if (prev.compressed_offset.get() + prev.compressed_size.get())
456 > curr.compressed_offset.get()
457 {
458 return Err(ChunkedArchiveError::IntegrityError);
459 }
460 }
461
462 for entry in entries.iter() {
464 if entry.decompressed_size.get() == 0 || entry.compressed_size.get() == 0 {
465 return Err(ChunkedArchiveError::IntegrityError);
466 }
467 }
468
469 for entry in entries.iter() {
471 let compressed_end = entry.compressed_offset.get() + entry.compressed_size.get();
472 if compressed_end > archive_length {
473 return Err(ChunkedArchiveError::IntegrityError);
474 }
475 }
476
477 let seek_table = entries
478 .iter()
479 .map(|entry| ChunkInfo::from_entry(entry, header_length))
480 .try_collect()?;
481 Ok(Some((DecodedArchive { seek_table, compression_algorithm }, chunk_data)))
482 }
483}
484
485pub struct CompressedChunk {
487 pub compressed_data: Vec<u8>,
489 pub decompressed_size: usize,
491}
492
493pub struct ChunkedArchive {
495 chunks: Vec<CompressedChunk>,
498 chunk_size: usize,
501 options: ChunkedArchiveOptions,
503}
504
505impl ChunkedArchive {
506 pub fn new(data: &[u8], options: ChunkedArchiveOptions) -> Result<Self, ChunkedArchiveError> {
510 let chunk_size = options.chunk_size_for(data.len());
511 let mut chunks: Vec<Result<CompressedChunk, ChunkedArchiveError>> = vec![];
512 let compressor = options.thread_local_compressor();
513 data.par_chunks(chunk_size)
514 .enumerate()
515 .map(|(index, chunk)| {
516 let compressed_data = compressor.compress(chunk, index)?;
517 Ok(CompressedChunk { compressed_data, decompressed_size: chunk.len() })
518 })
519 .collect_into_vec(&mut chunks);
520 let chunks: Vec<_> = chunks.into_iter().try_collect()?;
521 Ok(ChunkedArchive { chunks, chunk_size, options })
522 }
523
524 pub fn chunks(&self) -> &Vec<CompressedChunk> {
526 &self.chunks
527 }
528
529 pub fn chunk_size(&self) -> usize {
533 self.chunk_size
534 }
535
536 pub fn compressed_data_size(&self) -> usize {
538 self.chunks.iter().map(|chunk| chunk.compressed_data.len()).sum()
539 }
540
541 pub fn serialized_size(&self) -> usize {
543 ChunkedArchiveHeader::header_length(self.chunks.len()) + self.compressed_data_size()
544 }
545
546 pub fn write(self, mut writer: impl std::io::Write) -> Result<(), std::io::Error> {
548 let seek_table = self.make_seek_table();
549 let header = ChunkedArchiveHeader::new(&seek_table, self.options).unwrap();
550 writer.write_all(header.as_bytes())?;
551 writer.write_all(seek_table.as_slice().as_bytes())?;
552 for chunk in self.chunks {
553 writer.write_all(&chunk.compressed_data)?;
554 }
555 Ok(())
556 }
557
558 fn make_seek_table(&self) -> Vec<SeekTableEntry> {
560 let header_length = ChunkedArchiveHeader::header_length(self.chunks.len());
561 let mut seek_table = vec![];
562 seek_table.reserve(self.chunks.len());
563 let mut compressed_size: usize = 0;
564 let mut decompressed_offset: usize = 0;
565 for chunk in &self.chunks {
566 seek_table.push(SeekTableEntry {
567 decompressed_offset: (decompressed_offset as u64).into(),
568 decompressed_size: (chunk.decompressed_size as u64).into(),
569 compressed_offset: ((header_length + compressed_size) as u64).into(),
570 compressed_size: (chunk.compressed_data.len() as u64).into(),
571 });
572 compressed_size += chunk.compressed_data.len();
573 decompressed_offset += chunk.decompressed_size;
574 }
575 seek_table
576 }
577}
578
579pub struct ChunkedDecompressor {
594 seek_table: Vec<ChunkInfo>,
595 buffer: Vec<u8>,
596 data_written: usize,
597 curr_chunk: usize,
598 total_compressed_size: usize,
599 decompressor: Decompressor,
600 decompressed_buffer: Vec<u8>,
601 error_handler: Option<ErrorHandler>,
602}
603
604type ErrorHandler = Box<dyn Fn(usize, ChunkInfo, &[u8]) -> () + Send + 'static>;
605
606impl ChunkedDecompressor {
607 pub fn new(decoded_archive: DecodedArchive) -> Result<Self, ChunkedArchiveError> {
609 let DecodedArchive { compression_algorithm, seek_table } = decoded_archive;
610 let total_compressed_size =
611 seek_table.last().map_or(0, |last_chunk| last_chunk.compressed_range.end);
612 let decompressed_buffer =
613 vec![0u8; seek_table.first().map_or(0, |c| c.decompressed_range.len())];
614 Ok(Self {
615 seek_table,
616 buffer: vec![],
617 data_written: 0,
618 curr_chunk: 0,
619 total_compressed_size,
620 decompressor: compression_algorithm.decompressor(),
621 decompressed_buffer,
622 error_handler: None,
623 })
624 }
625
626 pub fn new_with_error_handler(
629 decoded_archive: DecodedArchive,
630 error_handler: ErrorHandler,
631 ) -> Result<Self, ChunkedArchiveError> {
632 Ok(Self { error_handler: Some(error_handler), ..Self::new(decoded_archive)? })
633 }
634
635 pub fn algorithm(&self) -> CompressionAlgorithm {
637 match &self.decompressor {
638 Decompressor::Zstd(_) => CompressionAlgorithm::Zstd,
639 Decompressor::Lz4 => CompressionAlgorithm::Lz4,
640 }
641 }
642
643 pub fn seek_table(&self) -> &Vec<ChunkInfo> {
644 &self.seek_table
645 }
646
647 fn finish_chunk(
648 &mut self,
649 data: &[u8],
650 chunk_callback: &mut impl FnMut(&[u8]) -> (),
651 ) -> Result<(), ChunkedArchiveError> {
652 debug_assert_eq!(data.len(), self.seek_table[self.curr_chunk].compressed_range.len());
653 let chunk = &self.seek_table[self.curr_chunk];
654 let decompressed_size = self
655 .decompressor
656 .decompress_into(data, self.decompressed_buffer.as_mut_slice(), self.curr_chunk)
657 .inspect_err(|_| {
658 if let Some(error_handler) = &self.error_handler {
659 error_handler(self.curr_chunk, chunk.clone(), data.as_bytes());
660 }
661 })?;
662 if decompressed_size != chunk.decompressed_range.len() {
663 return Err(ChunkedArchiveError::IntegrityError);
664 }
665 chunk_callback(&self.decompressed_buffer[..decompressed_size]);
666 self.curr_chunk += 1;
667 Ok(())
668 }
669
670 pub fn update(
672 &mut self,
673 mut data: &[u8],
674 chunk_callback: &mut impl FnMut(&[u8]) -> (),
675 ) -> Result<(), ChunkedArchiveError> {
676 if self.data_written + data.len() > self.total_compressed_size {
678 return Err(ChunkedArchiveError::OutOfRange);
679 }
680 self.data_written += data.len();
681
682 if !self.buffer.is_empty() {
684 let to_read = std::cmp::min(
685 data.len(),
686 self.seek_table[self.curr_chunk]
687 .compressed_range
688 .len()
689 .checked_sub(self.buffer.len())
690 .unwrap(),
691 );
692 self.buffer.extend_from_slice(&data[..to_read]);
693 if self.buffer.len() == self.seek_table[self.curr_chunk].compressed_range.len() {
694 let full_chunk = std::mem::take(&mut self.buffer);
698 self.finish_chunk(&full_chunk[..], chunk_callback)?;
699 self.buffer = full_chunk;
700 self.buffer.clear();
702 }
703 data = &data[to_read..];
704 }
705
706 while !data.is_empty()
708 && self.curr_chunk < self.seek_table.len()
709 && self.seek_table[self.curr_chunk].compressed_range.len() <= data.len()
710 {
711 let len = self.seek_table[self.curr_chunk].compressed_range.len();
712 self.finish_chunk(&data[..len], chunk_callback)?;
713 data = &data[len..];
714 }
715
716 if !data.is_empty() {
718 debug_assert!(self.curr_chunk < self.seek_table.len());
719 debug_assert!(self.data_written < self.total_compressed_size);
720 self.buffer.extend_from_slice(data);
721 }
722
723 debug_assert!(
724 self.data_written < self.total_compressed_size
725 || self.curr_chunk == self.seek_table.len()
726 );
727
728 Ok(())
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use crate::Type1Blob;
735
736 use super::*;
737 use rand::Rng;
738 use std::matches;
739
740 #[test]
743 fn compress_simple() {
744 let data: Vec<u8> = vec![0; 32 * 1024 * 16];
745 let archive = ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS).unwrap();
746 let mut compressed: Vec<u8> = vec![];
748 archive.write(&mut compressed).unwrap();
749 assert!(compressed.len() <= data.len());
750 assert!(decode_archive(&compressed, compressed.len()).unwrap().is_some());
752 }
753
754 fn generate_archive(
756 num_entries: usize,
757 options: ChunkedArchiveOptions,
758 ) -> (ChunkedArchiveHeader, Vec<SeekTableEntry>, u64) {
759 let mut seek_table = Vec::with_capacity(num_entries);
760 let header_length = ChunkedArchiveHeader::header_length(num_entries) as u64;
761 const COMPRESSED_CHUNK_SIZE: u64 = 1024;
762 const DECOMPRESSED_CHUNK_SIZE: u64 = 2048;
763 for n in 0..(num_entries as u64) {
764 seek_table.push(SeekTableEntry {
765 compressed_offset: (header_length + (n * COMPRESSED_CHUNK_SIZE)).into(),
766 compressed_size: COMPRESSED_CHUNK_SIZE.into(),
767 decompressed_offset: (n * DECOMPRESSED_CHUNK_SIZE).into(),
768 decompressed_size: DECOMPRESSED_CHUNK_SIZE.into(),
769 });
770 }
771 let header = ChunkedArchiveHeader::new(&seek_table, options).unwrap();
772 let archive_length: u64 = header_length + (num_entries as u64 * COMPRESSED_CHUNK_SIZE);
773 (header, seek_table, archive_length)
774 }
775
776 #[test]
777 fn should_validate_self() {
778 let (header, seek_table, archive_length) =
779 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
780 let serialized_table = seek_table.as_slice().as_bytes();
781 assert!(header.decode_archive(serialized_table, archive_length).unwrap().is_some());
782 }
783
784 #[test]
785 fn should_validate_empty() {
786 let (header, _, archive_length) = generate_archive(0, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
787 assert!(header.decode_archive(&[], archive_length).unwrap().is_some());
788 }
789
790 #[test]
791 fn should_detect_bad_magic() {
792 let (header, seek_table, archive_length) =
793 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
794 let mut corrupt_magic = ChunkedArchiveHeader::CHUNKED_ARCHIVE_MAGIC;
795 corrupt_magic[0] = !corrupt_magic[0];
796 let bad_magic = ChunkedArchiveHeader { magic: corrupt_magic, ..header };
797 let serialized_table = seek_table.as_slice().as_bytes();
798 assert!(matches!(
799 bad_magic.decode_archive(serialized_table, archive_length).unwrap_err(),
800 ChunkedArchiveError::BadMagic
801 ));
802 }
803 #[test]
804 fn should_detect_wrong_version() {
805 let (header, seek_table, archive_length) =
806 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
807 let invalid_version = ChunkedArchiveHeader { version: u16::MAX.into(), ..header };
808 let serialized_table = seek_table.as_slice().as_bytes();
809 assert!(matches!(
810 invalid_version.decode_archive(serialized_table, archive_length).unwrap_err(),
811 ChunkedArchiveError::InvalidVersion
812 ));
813 }
814
815 #[test]
816 fn should_detect_corrupt_checksum() {
817 let (header, seek_table, archive_length) =
818 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
819 let corrupt_checksum =
820 ChunkedArchiveHeader { checksum: (!header.checksum.get()).into(), ..header };
821 let serialized_table = seek_table.as_slice().as_bytes();
822 assert!(matches!(
823 corrupt_checksum.decode_archive(serialized_table, archive_length).unwrap_err(),
824 ChunkedArchiveError::IntegrityError
825 ));
826 }
827
828 #[test]
829 fn should_reject_too_many_entries_v2() {
830 let (too_many_entries, seek_table, archive_length) = generate_archive(
831 ChunkedArchiveOptions::V2_MAX_CHUNKS + 1,
832 Type1Blob::CHUNKED_ARCHIVE_OPTIONS,
833 );
834
835 let serialized_table = seek_table.as_slice().as_bytes();
836 assert!(matches!(
837 too_many_entries.decode_archive(serialized_table, archive_length).unwrap_err(),
838 ChunkedArchiveError::IntegrityError
839 ));
840 }
841
842 #[test]
843 fn invariant_i0_first_entry_zero() {
844 let (header, mut seek_table, archive_length) =
845 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
846 assert_eq!(seek_table[0].decompressed_offset.get(), 0);
847 seek_table[0].decompressed_offset = 1.into();
848
849 let serialized_table = seek_table.as_slice().as_bytes();
850 assert!(matches!(
851 header.decode_archive(serialized_table, archive_length).unwrap_err(),
852 ChunkedArchiveError::IntegrityError
853 ));
854 }
855
856 #[test]
857 fn invariant_i1_no_header_overlap() {
858 let (header, mut seek_table, archive_length) =
859 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
860 let header_end = ChunkedArchiveHeader::header_length(seek_table.len()) as u64;
861 assert!(seek_table[0].compressed_offset.get() >= header_end);
862 seek_table[0].compressed_offset = (header_end - 1).into();
863 let serialized_table = seek_table.as_slice().as_bytes();
864 assert!(matches!(
865 header.decode_archive(serialized_table, archive_length).unwrap_err(),
866 ChunkedArchiveError::IntegrityError
867 ));
868 }
869
870 #[test]
871 fn invariant_i2_decompressed_monotonic() {
872 let (header, mut seek_table, archive_length) =
873 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
874 assert_eq!(
875 seek_table[0].decompressed_offset.get() + seek_table[0].decompressed_size.get(),
876 seek_table[1].decompressed_offset.get()
877 );
878 seek_table[1].decompressed_offset = (seek_table[1].decompressed_offset.get() - 1).into();
879 let serialized_table = seek_table.as_slice().as_bytes();
880 assert!(matches!(
881 header.decode_archive(serialized_table, archive_length).unwrap_err(),
882 ChunkedArchiveError::IntegrityError
883 ));
884 }
885
886 #[test]
887 fn invariant_i3_compressed_monotonic() {
888 let (header, mut seek_table, archive_length) =
889 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
890 assert!(
891 (seek_table[0].compressed_offset.get() + seek_table[0].compressed_size.get())
892 <= seek_table[1].compressed_offset.get()
893 );
894 seek_table[1].compressed_offset = (seek_table[1].compressed_offset.get() - 1).into();
895 let serialized_table = seek_table.as_slice().as_bytes();
896 assert!(matches!(
897 header.decode_archive(serialized_table, archive_length).unwrap_err(),
898 ChunkedArchiveError::IntegrityError
899 ));
900 }
901
902 #[test]
903 fn invariant_i4_nonzero_compressed_size() {
904 let (header, mut seek_table, archive_length) =
905 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
906 assert!(seek_table[0].compressed_size.get() > 0);
907 seek_table[0].compressed_size = 0.into();
908 let serialized_table = seek_table.as_slice().as_bytes();
909 assert!(matches!(
910 header.decode_archive(serialized_table, archive_length).unwrap_err(),
911 ChunkedArchiveError::IntegrityError
912 ));
913 }
914
915 #[test]
916 fn invariant_i4_nonzero_decompressed_size() {
917 let (header, mut seek_table, archive_length) =
918 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
919 assert!(seek_table[0].decompressed_size.get() > 0);
920 seek_table[0].decompressed_size = 0.into();
921 let serialized_table = seek_table.as_slice().as_bytes();
922 assert!(matches!(
923 header.decode_archive(serialized_table, archive_length).unwrap_err(),
924 ChunkedArchiveError::IntegrityError
925 ));
926 }
927
928 #[test]
929 fn invariant_i5_within_archive() {
930 let (header, mut seek_table, archive_length) =
931 generate_archive(4, Type1Blob::CHUNKED_ARCHIVE_OPTIONS);
932 let last_entry = seek_table.last_mut().unwrap();
933 assert!(
934 (last_entry.compressed_offset.get() + last_entry.compressed_size.get())
935 <= archive_length
936 );
937 last_entry.compressed_offset = (archive_length + 1).into();
938 let serialized_table = seek_table.as_slice().as_bytes();
939 assert!(matches!(
940 header.decode_archive(serialized_table, archive_length).unwrap_err(),
941 ChunkedArchiveError::IntegrityError
942 ));
943 }
944
945 #[test]
946 fn max_chunks() {
947 let ChunkedArchiveOptions::V2 { minimum_chunk_size, chunk_alignment, .. } =
948 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
949 else {
950 panic!()
951 };
952 assert_eq!(
953 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
954 .chunk_size_for(minimum_chunk_size * ChunkedArchiveOptions::V2_MAX_CHUNKS),
955 minimum_chunk_size
956 );
957 assert_eq!(
958 Type1Blob::CHUNKED_ARCHIVE_OPTIONS
959 .chunk_size_for(minimum_chunk_size * ChunkedArchiveOptions::V2_MAX_CHUNKS + 1),
960 minimum_chunk_size + chunk_alignment
961 );
962 }
963
964 #[test]
965 fn test_decompressor_empty_archive() {
966 let mut compressed: Vec<u8> = vec![];
967 ChunkedArchive::new(&[], Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
968 .expect("compress")
969 .write(&mut compressed)
970 .expect("write archive");
971 let (decoded_archive, chunk_data) =
972 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
973 assert!(decoded_archive.seek_table.is_empty());
974 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
975 let mut chunk_callback = |_chunk: &[u8]| panic!("Archive doesn't have any chunks.");
976 chunk_data
978 .chunks(4)
979 .for_each(|data| decompressor.update(data, &mut chunk_callback).unwrap());
980 }
981
982 #[test]
983 fn test_decompressor() {
984 const UNCOMPRESSED_LENGTH: usize = 3_000_000;
985 let data: Vec<u8> = {
986 let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
987 rand::rng().sample_iter(&range).take(UNCOMPRESSED_LENGTH).collect()
988 };
989 let mut compressed: Vec<u8> = vec![];
990 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
991 .expect("compress")
992 .write(&mut compressed)
993 .expect("write archive");
994 let (decoded_archive, chunk_data) =
995 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
996
997 let num_chunks = decoded_archive.seek_table.len();
999 assert!(num_chunks > 1);
1000
1001 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1002
1003 let mut decoded_chunks: usize = 0;
1004 let mut decompressed_offset: usize = 0;
1005 let mut chunk_callback = |decompressed_chunk: &[u8]| {
1006 assert!(
1007 decompressed_chunk
1008 == &data[decompressed_offset..decompressed_offset + decompressed_chunk.len()]
1009 );
1010 decompressed_offset += decompressed_chunk.len();
1011 decoded_chunks += 1;
1012 };
1013
1014 chunk_data
1016 .chunks(4)
1017 .for_each(|data| decompressor.update(data, &mut chunk_callback).unwrap());
1018 assert_eq!(decoded_chunks, num_chunks);
1019 }
1020
1021 #[test]
1022 fn test_decompressor_corrupt_decompressed_size() {
1023 let data = vec![0; 3_000_000];
1024 let mut compressed: Vec<u8> = vec![];
1025 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
1026 .expect("compress")
1027 .write(&mut compressed)
1028 .expect("write archive");
1029 let (mut decoded_archive, chunk_data) =
1030 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1031
1032 decoded_archive.seek_table[0].decompressed_range =
1034 decoded_archive.seek_table[0].decompressed_range.start
1035 ..decoded_archive.seek_table[0].decompressed_range.end + 1;
1036
1037 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1038 assert!(matches!(
1039 decompressor.update(&chunk_data, &mut |_chunk| {}),
1040 Err(ChunkedArchiveError::IntegrityError)
1041 ));
1042 }
1043
1044 #[test]
1045 fn test_decompressor_corrupt_compressed_size() {
1046 let data = vec![0; 3_000_000];
1047 let mut compressed: Vec<u8> = vec![];
1048 ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS)
1049 .expect("compress")
1050 .write(&mut compressed)
1051 .expect("write archive");
1052 let (mut decoded_archive, chunk_data) =
1053 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1054
1055 decoded_archive.seek_table[0].compressed_range =
1057 decoded_archive.seek_table[0].compressed_range.start
1058 ..decoded_archive.seek_table[0].compressed_range.end - 1;
1059 let first_chunk_info = decoded_archive.seek_table[0].clone();
1060 let error_handler = move |chunk_index: usize, chunk_info: ChunkInfo, chunk_data: &[u8]| {
1061 assert_eq!(chunk_index, 0);
1062 assert_eq!(chunk_info, first_chunk_info);
1063 assert_eq!(chunk_data.len(), chunk_info.compressed_range.len());
1064 };
1065
1066 let mut decompressor =
1067 ChunkedDecompressor::new_with_error_handler(decoded_archive, Box::new(error_handler))
1068 .unwrap();
1069 assert!(matches!(
1070 decompressor.update(&chunk_data, &mut |_chunk| {}),
1071 Err(ChunkedArchiveError::DecompressionError { .. })
1072 ));
1073 }
1074
1075 #[test]
1076 fn test_decompressor_zstd_data_corruption() {
1077 let data = vec![0; 3_000_000];
1078 let mut compressed: Vec<u8> = vec![];
1079 let archive = match ChunkedArchive::new(&data, Type1Blob::CHUNKED_ARCHIVE_OPTIONS) {
1080 Ok(a) => a,
1081 Err(e) => {
1082 panic!("Failed to compress in test: {:?}", e);
1083 }
1084 };
1085 archive.write(&mut compressed).expect("write archive");
1086 let (decoded_archive, chunk_data) =
1087 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1088
1089 let mut corrupt_data = chunk_data.to_vec();
1090 if corrupt_data.len() > 100 {
1091 corrupt_data[100] = !corrupt_data[100];
1092 }
1093
1094 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1095 let result = decompressor.update(&corrupt_data, &mut |_chunk| {});
1096 assert!(matches!(result, Err(ChunkedArchiveError::DecompressionError { .. })));
1097 }
1098
1099 #[test]
1100 fn test_v3_zstd_roundtrip() {
1101 let data = vec![0; 3_000_000];
1102 let options =
1103 ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Zstd };
1104 let mut compressed = vec![];
1105 ChunkedArchive::new(&data, options)
1106 .expect("compress")
1107 .write(&mut compressed)
1108 .expect("write");
1109
1110 let (header, _) =
1112 Ref::<_, ChunkedArchiveHeader>::from_prefix(compressed.as_slice()).unwrap();
1113 assert_eq!(header.version.get(), 3);
1114 assert_eq!(header.compression_algorithm, CompressionAlgorithm::Zstd as u8);
1115
1116 let (decoded_archive, chunk_data) =
1117 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1118
1119 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1121 let mut decompressed: Vec<u8> = vec![];
1122 let mut chunk_callback = |chunk: &[u8]| decompressed.extend_from_slice(chunk);
1123 decompressor.update(chunk_data, &mut chunk_callback).unwrap();
1124
1125 assert_eq!(decompressed, data);
1126 }
1127
1128 #[test]
1129 fn test_v3_lz4_roundtrip() {
1130 let data = vec![0; 3_000_000];
1131 let options =
1132 ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Lz4 };
1133 let mut compressed = vec![];
1134 ChunkedArchive::new(&data, options)
1135 .expect("compress")
1136 .write(&mut compressed)
1137 .expect("write");
1138
1139 let (header, _) =
1141 Ref::<_, ChunkedArchiveHeader>::from_prefix(compressed.as_slice()).unwrap();
1142 assert_eq!(header.version.get(), 3);
1143 assert_eq!(header.compression_algorithm, CompressionAlgorithm::Lz4 as u8);
1144
1145 let (decoded_archive, chunk_data) =
1146 decode_archive(&compressed, compressed.len()).unwrap().unwrap();
1147
1148 let mut decompressor = ChunkedDecompressor::new(decoded_archive).unwrap();
1150 let mut decompressed: Vec<u8> = vec![];
1151 let mut chunk_callback = |chunk: &[u8]| decompressed.extend_from_slice(chunk);
1152 decompressor.update(chunk_data, &mut chunk_callback).unwrap();
1153
1154 assert_eq!(decompressed, data);
1155 }
1156}