Skip to main content

delivery_blob/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Library for creating, serializing, and deserializing RFC 0207 delivery blobs. For example, to
6//! create a Type 1 delivery blob:
7//!
8//! ```
9//! use delivery_blob::{CompressionMode, Type1Blob};
10//! let merkle = "68d131bc271f9c192d4f6dcd8fe61bef90004856da19d0f2f514a7f4098b0737";
11//! let data: Vec<u8> = vec![0xFF; 8192];
12//! let payload: Vec<u8> = Type1Blob::generate(&data, CompressionMode::Attempt);
13//! ```
14
15use crate::compression::{ChunkedArchive, ChunkedArchiveOptions, ChunkedDecompressor};
16use crate::format::{SerializedType1Blob, SerializedType3Blob};
17use serde::{Deserialize, Serialize};
18use static_assertions::assert_eq_size;
19use thiserror::Error;
20use zerocopy::{IntoBytes, Ref};
21
22pub mod compression;
23pub use compression::DataBuffer;
24mod format;
25
26// This library assumes usize is large enough to hold a u64.
27assert_eq_size!(usize, u64);
28
29/// Generate a delivery blob of the specified `delivery_type` for `data` using default parameters.
30pub fn generate(delivery_type: DeliveryBlobType, data: &[u8]) -> Vec<u8> {
31    match delivery_type {
32        DeliveryBlobType::Type1 => Type1Blob::generate(data, CompressionMode::Attempt),
33        DeliveryBlobType::Type2 => Type2Blob::generate(data, CompressionMode::Attempt),
34        DeliveryBlobType::Type3 => Type3Blob::generate(data, CompressionMode::Attempt),
35        _ => panic!("Unsupported delivery blob type: {:?}", delivery_type),
36    }
37}
38
39/// Generate a delivery blob of the specified `delivery_type` for `data` using default parameters
40/// and write the generated blob to `writer`.
41pub fn generate_to(
42    delivery_type: DeliveryBlobType,
43    data: &[u8],
44    writer: impl std::io::Write,
45) -> Result<(), std::io::Error> {
46    match delivery_type {
47        DeliveryBlobType::Type1 => Type1Blob::generate_to(data, CompressionMode::Attempt, writer),
48        DeliveryBlobType::Type2 => Type2Blob::generate_to(data, CompressionMode::Attempt, writer),
49        DeliveryBlobType::Type3 => Type3Blob::generate_to(data, CompressionMode::Attempt, writer),
50        _ => panic!("Unsupported delivery blob type: {:?}", delivery_type),
51    }
52}
53
54/// Returns the type of delivery blob represented by `data`.
55/// Returns an error if `data` is too short to contain a valid header.
56///
57/// **Note**: Only the header portion of the delivery blob needs to be present
58/// in `data` (the full payload is not required).
59pub fn delivery_blob_type(data: &[u8]) -> Result<DeliveryBlobType, DecompressError> {
60    let header = DeliveryBlobHeader::parse(data)?.ok_or(DecompressError::NeedMoreData)?;
61    Ok(header.delivery_type)
62}
63
64/// Returns the decompressed size of `delivery_blob`, delivery blob type is auto detected.
65pub fn decompressed_size(delivery_blob: &[u8]) -> Result<u64, DecompressError> {
66    DeliveryBlob::decompressed_size(delivery_blob)
67}
68
69/// Returns the decompressed size of the delivery blob from `reader`.
70pub fn decompressed_size_from_reader(
71    mut reader: impl std::io::Read,
72) -> Result<u64, DecompressError> {
73    let mut buf = vec![];
74    loop {
75        let already_read = buf.len();
76        let new_size = already_read + 4096;
77        buf.resize(new_size, 0);
78        let new_size = already_read + reader.read(&mut buf[already_read..new_size])?;
79        if new_size == already_read {
80            return Err(DecompressError::NeedMoreData);
81        }
82        buf.truncate(new_size);
83        match decompressed_size(&buf) {
84            Ok(size) => {
85                return Ok(size);
86            }
87            Err(DecompressError::NeedMoreData) => {}
88            Err(e) => {
89                return Err(e);
90            }
91        }
92    }
93}
94
95/// Decompress a delivery blob in `delivery_blob`, delivery blob type is auto detected.
96pub fn decompress(delivery_blob: &[u8]) -> Result<Vec<u8>, DecompressError> {
97    DeliveryBlob::decompress(delivery_blob)
98}
99
100/// Decompress a delivery blob in `delivery_blob`, and write the decompressed blob to `writer`,
101/// delivery blob type is auto detected.
102pub fn decompress_to(
103    delivery_blob: &[u8],
104    writer: impl std::io::Write,
105) -> Result<(), DecompressError> {
106    DeliveryBlob::decompress_to(delivery_blob, writer)
107}
108
109/// Calculate the merkle root digest of the decompressed `delivery_blob`, delivery blob type is auto
110/// detected.
111pub fn calculate_digest(delivery_blob: &[u8]) -> Result<fuchsia_merkle::Hash, DecompressError> {
112    let mut writer = fuchsia_merkle::BufferedMerkleRootBuilder::default();
113    let () = DeliveryBlob::decompress_to(delivery_blob, &mut writer)?;
114    Ok(writer.complete())
115}
116
117#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
118pub enum DeliveryBlobError {
119    #[error("Invalid or unsupported delivery blob type.")]
120    InvalidType,
121
122    #[error("Delivery blob header has incorrect magic.")]
123    BadMagic,
124
125    #[error("Integrity/checksum or other validity checks failed.")]
126    IntegrityError,
127}
128
129#[derive(Debug, Error)]
130pub enum DecompressError {
131    #[error("DeliveryBlob error")]
132    DeliveryBlob(#[from] DeliveryBlobError),
133
134    #[error("ChunkedArchive error")]
135    ChunkedArchive(#[from] compression::ChunkedArchiveError),
136
137    #[error("Need more data")]
138    NeedMoreData,
139
140    #[error("io error")]
141    IoError(#[from] std::io::Error),
142}
143
144#[cfg(target_os = "fuchsia")]
145impl From<DeliveryBlobError> for zx::Status {
146    fn from(value: DeliveryBlobError) -> Self {
147        match value {
148            // Unsupported delivery blob type.
149            DeliveryBlobError::InvalidType => zx::Status::NOT_SUPPORTED,
150            // Potentially corrupted delivery blob.
151            DeliveryBlobError::BadMagic | DeliveryBlobError::IntegrityError => {
152                zx::Status::IO_DATA_INTEGRITY
153            }
154        }
155    }
156}
157
158/// Typed header of an RFC 0207 compliant delivery blob.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct DeliveryBlobHeader {
161    pub delivery_type: DeliveryBlobType,
162    pub header_length: u32,
163}
164
165impl DeliveryBlobHeader {
166    /// Attempt to parse `data` as a delivery blob. On success, returns validated blob header.
167    /// **WARNING**: This function does not verify that the payload is complete. Only the full
168    /// header of a delivery blob are required to be present in `data`.
169    pub fn parse(data: &[u8]) -> Result<Option<DeliveryBlobHeader>, DeliveryBlobError> {
170        let Ok((serialized_header, _metadata_and_payload)) =
171            Ref::<_, format::SerializedHeader>::from_prefix(data)
172        else {
173            return Ok(None);
174        };
175        serialized_header.decode().map(Some)
176    }
177}
178
179/// Type of delivery blob.
180///
181/// **WARNING**: These constants are used when generating delivery blobs and should not be changed.
182/// Non backwards-compatible changes to delivery blob formats should be made by creating a new type.
183#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
184#[repr(u32)]
185pub enum DeliveryBlobType {
186    /// Reserved for internal use.
187    Reserved = 0,
188    /// Type 1 delivery blobs use zstd-chunked compression with level 14 and 32KiB chunk size.
189    Type1 = 1,
190    /// Type 2 delivery blobs use zstd-chunked compression with level 21 and 128KiB chunk size.
191    Type2 = 2,
192    /// Type 3 delivery blobs support the lz4-chunked compression format.
193    /// NOTE: Type 3 delivery blobs are currently UNSTABLE / EXPERIMENTAL and subject to change.
194    Type3 = 3,
195}
196
197impl TryFrom<u32> for DeliveryBlobType {
198    type Error = DeliveryBlobError;
199    fn try_from(value: u32) -> Result<Self, Self::Error> {
200        match value {
201            value if value == DeliveryBlobType::Reserved as u32 => Ok(DeliveryBlobType::Reserved),
202            value if value == DeliveryBlobType::Type1 as u32 => Ok(DeliveryBlobType::Type1),
203            value if value == DeliveryBlobType::Type2 as u32 => Ok(DeliveryBlobType::Type2),
204            value if value == DeliveryBlobType::Type3 as u32 => Ok(DeliveryBlobType::Type3),
205            _ => Err(DeliveryBlobError::InvalidType),
206        }
207    }
208}
209
210impl From<DeliveryBlobType> for u32 {
211    fn from(value: DeliveryBlobType) -> Self {
212        value as u32
213    }
214}
215
216/// Mode specifying when a delivery blob should be compressed.
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub enum CompressionMode {
219    /// Never compress input, output uncompressed.
220    Never,
221    /// Compress input, output compressed if saves space, otherwise uncompressed.
222    Attempt,
223    /// Compress input, output compressed unconditionally (even if space is wasted).
224    Always,
225}
226
227/// Untyped header + metadata fields of an RFC 0207 delivery blob.
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229pub struct DeliveryBlob {
230    pub header: DeliveryBlobHeader,
231    pub payload_length: usize,
232    pub is_compressed: bool,
233}
234
235impl DeliveryBlob {
236    /// Attempt to parse `data` as a delivery blob. On success, returns validated blob info,
237    /// and the remainder of `data` representing the blob payload.
238    ///
239    /// If `allow_type3` is false, Type 3 delivery blobs will return
240    /// `DeliveryBlobError::InvalidType`.
241    pub fn parse(
242        data: &[u8],
243        allow_type3: bool,
244    ) -> Result<Option<(DeliveryBlob, &[u8])>, DeliveryBlobError> {
245        let Some(header) = DeliveryBlobHeader::parse(data)? else {
246            return Ok(None);
247        };
248        match header.delivery_type {
249            DeliveryBlobType::Type1 | DeliveryBlobType::Type2 => {
250                let Ok((serialized_header, payload)) =
251                    Ref::<_, format::SerializedType1Blob>::from_prefix(data)
252                else {
253                    return Ok(None);
254                };
255                serialized_header.decode().map(|metadata| Some((metadata, payload)))
256            }
257            DeliveryBlobType::Type3 => {
258                if !allow_type3 {
259                    return Err(DeliveryBlobError::InvalidType);
260                }
261                let Ok((serialized_header, payload)) =
262                    Ref::<_, format::SerializedType3Blob>::from_prefix(data)
263                else {
264                    return Ok(None);
265                };
266                serialized_header.decode().map(|metadata| Some((metadata.into(), payload)))
267            }
268            _ => Err(DeliveryBlobError::InvalidType),
269        }
270    }
271
272    /// Return the decompressed size of the blob without decompressing it.
273    pub fn decompressed_size(delivery_blob: &[u8]) -> Result<u64, DecompressError> {
274        let (header, payload) =
275            Self::parse(delivery_blob, true)?.ok_or(DecompressError::NeedMoreData)?;
276        if !header.is_compressed {
277            return Ok(header.payload_length as u64);
278        }
279
280        let (decoded_archive, _chunk_data) =
281            compression::decode_archive(payload, header.payload_length)?
282                .ok_or(DecompressError::NeedMoreData)?;
283        Ok(decoded_archive.decompressed_size() as u64)
284    }
285
286    /// Decompress a delivery blob in `delivery_blob`.
287    pub fn decompress(delivery_blob: &[u8]) -> Result<Vec<u8>, DecompressError> {
288        let mut decompressed = vec![];
289        decompressed.reserve(Self::decompressed_size(delivery_blob)? as usize);
290        Self::decompress_to(delivery_blob, &mut decompressed)?;
291        Ok(decompressed)
292    }
293
294    /// Decompress a delivery blob in `delivery_blob` to `writer`.
295    pub fn decompress_to(
296        delivery_blob: &[u8],
297        mut writer: impl std::io::Write,
298    ) -> Result<(), DecompressError> {
299        let (header, payload) =
300            Self::parse(delivery_blob, true)?.ok_or(DecompressError::NeedMoreData)?;
301        if !header.is_compressed {
302            return Ok(writer.write_all(payload)?);
303        }
304
305        let (decoded_archive, chunk_data) =
306            compression::decode_archive(payload, header.payload_length)?
307                .ok_or(DecompressError::NeedMoreData)?;
308        let mut decompressor = ChunkedDecompressor::new(decoded_archive)?;
309        let mut result = Ok(());
310        let mut chunk_callback = |chunk: &[u8]| {
311            if let Err(e) = writer.write_all(chunk) {
312                result = Err(e.into());
313            }
314        };
315        decompressor.update(chunk_data, &mut chunk_callback)?;
316        result
317    }
318}
319
320/// Header + metadata fields of a Type 1 blob.
321///
322/// **WARNING**: Outside of storage-owned components, this should only be used for informational
323/// or debugging purposes. The contents of this struct should be considered internal implementation
324/// details and are subject to change at any time.
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub struct Type1Blob {
327    pub header: DeliveryBlobHeader,
328    pub payload_length: usize,
329    pub is_compressed: bool,
330}
331
332impl From<DeliveryBlob> for Type1Blob {
333    fn from(blob: DeliveryBlob) -> Self {
334        Self {
335            header: blob.header,
336            payload_length: blob.payload_length,
337            is_compressed: blob.is_compressed,
338        }
339    }
340}
341
342impl From<Type1Blob> for DeliveryBlob {
343    fn from(blob: Type1Blob) -> Self {
344        Self {
345            header: blob.header,
346            payload_length: blob.payload_length,
347            is_compressed: blob.is_compressed,
348        }
349    }
350}
351
352impl Type1Blob {
353    pub const HEADER: DeliveryBlobHeader = DeliveryBlobHeader {
354        delivery_type: DeliveryBlobType::Type1,
355        header_length: std::mem::size_of::<SerializedType1Blob>() as u32,
356    };
357
358    pub const CHUNKED_ARCHIVE_OPTIONS: ChunkedArchiveOptions = ChunkedArchiveOptions::V2 {
359        chunk_alignment: fuchsia_merkle::BLOCK_SIZE,
360        minimum_chunk_size: 32 * 1024,
361        compression_level: 14,
362    };
363
364    /// Generate a Type 1 delivery blob for `data` using the specified `mode`.
365    pub fn generate(data: &[u8], mode: CompressionMode) -> Vec<u8> {
366        let mut delivery_blob: Vec<u8> = vec![];
367        Self::generate_to(data, mode, &mut delivery_blob).unwrap();
368        delivery_blob
369    }
370
371    /// Generate a Type 1 delivery blob for `data` using the specified `mode`. Writes delivery blob
372    /// directly into `writer`.
373    pub fn generate_to(
374        data: &[u8],
375        mode: CompressionMode,
376        writer: impl std::io::Write,
377    ) -> Result<(), std::io::Error> {
378        generate_blob_to(Self::HEADER, Self::CHUNKED_ARCHIVE_OPTIONS, data, mode, writer)
379    }
380
381    /// Attempt to parse `data` as a Type 1 delivery blob. On success, returns validated blob info,
382    /// and the remainder of `data` representing the blob payload.
383    pub fn parse(data: &[u8]) -> Result<Option<(Type1Blob, &[u8])>, DeliveryBlobError> {
384        match DeliveryBlob::parse(data, true)? {
385            Some((blob, payload)) if blob.header.delivery_type == DeliveryBlobType::Type1 => {
386                Ok(Some((blob.into(), payload)))
387            }
388            Some(_) => Err(DeliveryBlobError::InvalidType),
389            None => Ok(None),
390        }
391    }
392}
393
394/// Header + metadata fields of a Type 2 blob.
395///
396/// **WARNING**: Outside of storage-owned components, this should only be used for informational
397/// or debugging purposes. The contents of this struct should be considered internal implementation
398/// details and are subject to change at any time.
399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
400pub struct Type2Blob {
401    pub header: DeliveryBlobHeader,
402    pub payload_length: usize,
403    pub is_compressed: bool,
404}
405
406impl From<DeliveryBlob> for Type2Blob {
407    fn from(blob: DeliveryBlob) -> Self {
408        Self {
409            header: blob.header,
410            payload_length: blob.payload_length,
411            is_compressed: blob.is_compressed,
412        }
413    }
414}
415
416impl From<Type2Blob> for DeliveryBlob {
417    fn from(blob: Type2Blob) -> Self {
418        Self {
419            header: blob.header,
420            payload_length: blob.payload_length,
421            is_compressed: blob.is_compressed,
422        }
423    }
424}
425
426impl Type2Blob {
427    pub const HEADER: DeliveryBlobHeader = DeliveryBlobHeader {
428        delivery_type: DeliveryBlobType::Type2,
429        header_length: std::mem::size_of::<SerializedType1Blob>() as u32,
430    };
431
432    pub const CHUNKED_ARCHIVE_OPTIONS: ChunkedArchiveOptions = ChunkedArchiveOptions::V2 {
433        chunk_alignment: fuchsia_merkle::BLOCK_SIZE,
434        minimum_chunk_size: 128 * 1024,
435        compression_level: 21,
436    };
437
438    /// Generate a Type 2 delivery blob for `data` using the specified `mode`.
439    pub fn generate(data: &[u8], mode: CompressionMode) -> Vec<u8> {
440        let mut delivery_blob: Vec<u8> = vec![];
441        Self::generate_to(data, mode, &mut delivery_blob).unwrap();
442        delivery_blob
443    }
444
445    /// Generate a Type 2 delivery blob for `data` using the specified `mode`. Writes delivery blob
446    /// directly into `writer`.
447    pub fn generate_to(
448        data: &[u8],
449        mode: CompressionMode,
450        writer: impl std::io::Write,
451    ) -> Result<(), std::io::Error> {
452        generate_blob_to(Self::HEADER, Self::CHUNKED_ARCHIVE_OPTIONS, data, mode, writer)
453    }
454
455    /// Attempt to parse `data` as a Type 2 delivery blob. On success, returns validated blob info,
456    /// and the remainder of `data` representing the blob payload.
457    pub fn parse(data: &[u8]) -> Result<Option<(Type2Blob, &[u8])>, DeliveryBlobError> {
458        match DeliveryBlob::parse(data, true)? {
459            Some((blob, payload)) if blob.header.delivery_type == DeliveryBlobType::Type2 => {
460                Ok(Some((blob.into(), payload)))
461            }
462            Some(_) => Err(DeliveryBlobError::InvalidType),
463            None => Ok(None),
464        }
465    }
466}
467
468/// Header + metadata fields of a Type 3 blob.
469///
470/// **NOTE**: Type 3 delivery blobs are currently UNSTABLE / EXPERIMENTAL and subject to change.
471///
472/// **WARNING**: Outside of storage-owned components, this should only be used for informational
473/// or debugging purposes. The contents of this struct should be considered internal implementation
474/// details and are subject to change at any time.
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476pub struct Type3Blob {
477    pub header: DeliveryBlobHeader,
478    pub payload_length: usize,
479    pub is_compressed: bool,
480}
481
482impl From<DeliveryBlob> for Type3Blob {
483    fn from(blob: DeliveryBlob) -> Self {
484        Self {
485            header: blob.header,
486            payload_length: blob.payload_length,
487            is_compressed: blob.is_compressed,
488        }
489    }
490}
491
492impl From<Type3Blob> for DeliveryBlob {
493    fn from(blob: Type3Blob) -> Self {
494        Self {
495            header: blob.header,
496            payload_length: blob.payload_length,
497            is_compressed: blob.is_compressed,
498        }
499    }
500}
501
502impl Type3Blob {
503    pub const HEADER: DeliveryBlobHeader = DeliveryBlobHeader {
504        delivery_type: DeliveryBlobType::Type3,
505        header_length: std::mem::size_of::<SerializedType3Blob>() as u32,
506    };
507
508    pub const CHUNKED_ARCHIVE_OPTIONS: ChunkedArchiveOptions =
509        ChunkedArchiveOptions::V3 { compression_algorithm: compression::CompressionAlgorithm::Lz4 };
510
511    /// Generate a Type 3 delivery blob for `data` using the specified `mode`.
512    pub fn generate(data: &[u8], mode: CompressionMode) -> Vec<u8> {
513        let mut delivery_blob: Vec<u8> = vec![];
514        Self::generate_to(data, mode, &mut delivery_blob).unwrap();
515        delivery_blob
516    }
517
518    /// Generate a Type 3 delivery blob for `data` using the specified `mode`. Writes delivery blob
519    /// directly into `writer`.
520    pub fn generate_to(
521        data: &[u8],
522        mode: CompressionMode,
523        mut writer: impl std::io::Write,
524    ) -> Result<(), std::io::Error> {
525        let compressed = match mode {
526            CompressionMode::Attempt | CompressionMode::Always => {
527                let compressed = ChunkedArchive::new(data, Self::CHUNKED_ARCHIVE_OPTIONS)
528                    .expect("failed to compress data");
529                if mode == CompressionMode::Always || compressed.serialized_size() <= data.len() {
530                    Some(compressed)
531                } else {
532                    None
533                }
534            }
535            CompressionMode::Never => None,
536        };
537
538        let payload_length =
539            compressed.as_ref().map(|archive| archive.serialized_size()).unwrap_or(data.len());
540        let header =
541            Self { header: Self::HEADER, payload_length, is_compressed: compressed.is_some() };
542        let serialized_header: SerializedType3Blob = header.into();
543        writer.write_all(serialized_header.as_bytes())?;
544
545        if let Some(archive) = compressed {
546            archive.write(writer)?;
547        } else {
548            writer.write_all(data)?;
549        }
550        Ok(())
551    }
552
553    /// Attempt to parse `data` as a Type 3 delivery blob. On success, returns validated blob info,
554    /// and the remainder of `data` representing the blob payload.
555    pub fn parse(data: &[u8]) -> Result<Option<(Type3Blob, &[u8])>, DeliveryBlobError> {
556        let Ok((serialized_header, payload)) = Ref::<_, SerializedType3Blob>::from_prefix(data)
557        else {
558            return Ok(None);
559        };
560        serialized_header.decode().map(|metadata| Some((metadata, payload)))
561    }
562}
563
564fn generate_blob_to(
565    header_info: DeliveryBlobHeader,
566    options: ChunkedArchiveOptions,
567    data: &[u8],
568    mode: CompressionMode,
569    mut writer: impl std::io::Write,
570) -> Result<(), std::io::Error> {
571    let compressed = match mode {
572        CompressionMode::Attempt | CompressionMode::Always => {
573            let compressed = ChunkedArchive::new(data, options).expect("failed to compress data");
574            if mode == CompressionMode::Always || compressed.serialized_size() <= data.len() {
575                Some(compressed)
576            } else {
577                None
578            }
579        }
580        CompressionMode::Never => None,
581    };
582
583    let payload_length =
584        compressed.as_ref().map(|archive| archive.serialized_size()).unwrap_or(data.len());
585    let blob =
586        DeliveryBlob { header: header_info, payload_length, is_compressed: compressed.is_some() };
587    let serialized_header: SerializedType1Blob = blob.into();
588    writer.write_all(serialized_header.as_bytes())?;
589
590    if let Some(archive) = compressed {
591        archive.write(writer)?;
592    } else {
593        writer.write_all(data)?;
594    }
595    Ok(())
596}
597
598pub const MINIMUM_HEADER_SIZE: u32 = Type1Blob::HEADER.header_length;
599
600#[cfg(test)]
601mod tests {
602
603    use super::*;
604    use rand::Rng;
605
606    const DATA_LEN: usize = 500_000;
607
608    #[test]
609    fn compression_mode_never() {
610        let data: Vec<u8> = vec![0; DATA_LEN];
611        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Never);
612        // Payload should be uncompressed and have the same size as the original input data.
613        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
614        assert!(!header.is_compressed);
615        assert_eq!(header.payload_length, data.len());
616        assert_eq!(decompress(&delivery_blob).unwrap(), data);
617    }
618
619    #[test]
620    fn compression_mode_always() {
621        let data: Vec<u8> = {
622            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
623            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
624        };
625        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
626        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
627        // Payload is not very compressible, so we expect it to be larger than the original.
628        assert!(header.is_compressed);
629        assert!(header.payload_length > data.len());
630        assert_eq!(decompress(&delivery_blob).unwrap(), data);
631    }
632
633    #[test]
634    fn compression_mode_attempt_uncompressible() {
635        let data: Vec<u8> = {
636            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
637            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
638        };
639        // Data is random and therefore shouldn't be very compressible.
640        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Attempt);
641        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
642        assert!(!header.is_compressed);
643        assert_eq!(header.payload_length, data.len());
644        assert_eq!(decompress(&delivery_blob).unwrap(), data);
645    }
646
647    #[test]
648    fn compression_mode_attempt_compressible() {
649        let data: Vec<u8> = vec![0; DATA_LEN];
650        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Attempt);
651        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
652        // Payload should be compressed and smaller than the original input.
653        assert!(header.is_compressed);
654        assert!(header.payload_length < data.len());
655        assert_eq!(decompress(&delivery_blob).unwrap(), data);
656    }
657
658    #[test]
659    fn get_decompressed_size() {
660        let data: Vec<u8> = {
661            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
662            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
663        };
664        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
665        assert_eq!(decompressed_size(&delivery_blob).unwrap(), DATA_LEN as u64);
666        assert_eq!(decompressed_size_from_reader(&delivery_blob[..]).unwrap(), DATA_LEN as u64);
667    }
668
669    #[test]
670    fn get_delivery_blob_type() {
671        let delivery_blob = Type1Blob::generate(&[], CompressionMode::Never);
672        assert_eq!(delivery_blob_type(&delivery_blob).unwrap(), DeliveryBlobType::Type1);
673    }
674
675    #[test]
676    fn get_delivery_blob_type_truncated() {
677        let delivery_blob = Type1Blob::generate(&[], CompressionMode::Never);
678        // Truncate the blob to 2 bytes, which is too short to contain a valid header.
679        assert!(matches!(
680            delivery_blob_type(&delivery_blob[..2]).unwrap_err(),
681            DecompressError::NeedMoreData
682        ));
683    }
684
685    #[test]
686    fn get_delivery_blob_type_corrupted() {
687        // Provide enough bytes to bypass the length check (64 bytes), but fill them with garbage.
688        let bad_data = vec![0xFF; 64];
689
690        // This should fail to parse the header (usually returning BadMagic or InvalidType).
691        assert!(matches!(
692            delivery_blob_type(&bad_data).unwrap_err(),
693            DecompressError::DeliveryBlob(_)
694        ));
695    }
696
697    #[test]
698    fn test_calculate_digest() {
699        let data: Vec<u8> = {
700            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
701            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
702        };
703        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
704        assert_eq!(
705            calculate_digest(&delivery_blob).unwrap(),
706            fuchsia_merkle::root_from_slice(&data)
707        );
708    }
709
710    #[test]
711    fn type_2_round_trip() {
712        let data: Vec<u8> = vec![0x42; DATA_LEN];
713        let delivery_blob = Type2Blob::generate(&data, CompressionMode::Attempt);
714        let (header, _) = Type2Blob::parse(&delivery_blob).unwrap().unwrap();
715        assert_eq!(header.header.delivery_type, DeliveryBlobType::Type2);
716        assert!(header.is_compressed);
717        assert_eq!(decompress(&delivery_blob).unwrap(), data);
718    }
719
720    #[test]
721    fn type_2_vs_type_1_chunk_size() {
722        let data: Vec<u8> = vec![0x42; 256 * 1024];
723        let blob_v1 = Type1Blob::generate(&data, CompressionMode::Always);
724        let blob_v2 = Type2Blob::generate(&data, CompressionMode::Always);
725
726        let (_, payload_v1) = DeliveryBlob::parse(&blob_v1, true).unwrap().unwrap();
727        let (decoded_v1, _) =
728            compression::decode_archive(payload_v1, payload_v1.len()).unwrap().unwrap();
729        assert_eq!(decoded_v1.seek_table().len(), 8);
730
731        let (_, payload_v2) = DeliveryBlob::parse(&blob_v2, true).unwrap().unwrap();
732        let (decoded_v2, _) =
733            compression::decode_archive(payload_v2, payload_v2.len()).unwrap().unwrap();
734        assert_eq!(decoded_v2.seek_table().len(), 2);
735    }
736
737    #[test]
738    fn type_3_compression_mode_never() {
739        let data: Vec<u8> = vec![0; DATA_LEN];
740        let delivery_blob = Type3Blob::generate(&data, CompressionMode::Never);
741        let (header, _) = Type3Blob::parse(&delivery_blob).unwrap().unwrap();
742        assert!(!header.is_compressed);
743        assert_eq!(header.payload_length, data.len());
744        assert_eq!(decompress(&delivery_blob).unwrap(), data);
745    }
746
747    #[test]
748    fn type_3_compression_mode_always() {
749        let data: Vec<u8> = {
750            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
751            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
752        };
753        let delivery_blob = Type3Blob::generate(&data, CompressionMode::Always);
754        let (header, _) = Type3Blob::parse(&delivery_blob).unwrap().unwrap();
755        assert!(header.is_compressed);
756        assert!(header.payload_length > data.len());
757        assert_eq!(decompress(&delivery_blob).unwrap(), data);
758    }
759
760    #[test]
761    fn type_3_compression_mode_attempt_compressible() {
762        let data: Vec<u8> = vec![0; DATA_LEN];
763        let delivery_blob = Type3Blob::generate(&data, CompressionMode::Attempt);
764        let (header, _) = Type3Blob::parse(&delivery_blob).unwrap().unwrap();
765        assert!(header.is_compressed);
766        assert!(header.payload_length < data.len());
767        assert_eq!(decompress(&delivery_blob).unwrap(), data);
768    }
769
770    #[test]
771    fn type_3_get_decompressed_size_and_digest() {
772        let data: Vec<u8> = {
773            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
774            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
775        };
776        let delivery_blob = generate(DeliveryBlobType::Type3, &data);
777        assert_eq!(decompressed_size(&delivery_blob).unwrap(), DATA_LEN as u64);
778        assert_eq!(
779            calculate_digest(&delivery_blob).unwrap(),
780            fuchsia_merkle::root_from_slice(&data)
781        );
782        assert_eq!(decompress(&delivery_blob).unwrap(), data);
783    }
784
785    #[test]
786    fn test_delivery_blob_parse() {
787        let data: Vec<u8> = vec![1, 2, 3, 4];
788        let type1_blob = generate(DeliveryBlobType::Type1, &data);
789        let type3_blob = generate(DeliveryBlobType::Type3, &data);
790
791        assert!(DeliveryBlob::parse(&type1_blob, false).unwrap().is_some());
792        assert!(DeliveryBlob::parse(&type1_blob, true).unwrap().is_some());
793
794        assert_eq!(
795            DeliveryBlob::parse(&type3_blob, false).unwrap_err(),
796            DeliveryBlobError::InvalidType
797        );
798        assert!(DeliveryBlob::parse(&type3_blob, true).unwrap().is_some());
799    }
800}