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