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