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;
17use serde::{Deserialize, Serialize};
18use static_assertions::assert_eq_size;
19use thiserror::Error;
20use zerocopy::{IntoBytes, Ref};
21
22pub mod compression;
23mod format;
24
25// This library assumes usize is large enough to hold a u64.
26assert_eq_size!(usize, u64);
27
28/// Generate a delivery blob of the specified `delivery_type` for `data` using default parameters.
29pub fn generate(delivery_type: DeliveryBlobType, data: &[u8]) -> Vec<u8> {
30    match delivery_type {
31        DeliveryBlobType::Type1 => Type1Blob::generate(data, CompressionMode::Attempt),
32        _ => panic!("Unsupported delivery blob type: {:?}", delivery_type),
33    }
34}
35
36/// Generate a delivery blob of the specified `delivery_type` for `data` using default parameters
37/// and write the generated blob to `writer`.
38pub fn generate_to(
39    delivery_type: DeliveryBlobType,
40    data: &[u8],
41    writer: impl std::io::Write,
42) -> Result<(), std::io::Error> {
43    match delivery_type {
44        DeliveryBlobType::Type1 => Type1Blob::generate_to(data, CompressionMode::Attempt, writer),
45        _ => panic!("Unsupported delivery blob type: {:?}", delivery_type),
46    }
47}
48
49/// Returns the decompressed size of `delivery_blob`, delivery blob type is auto detected.
50pub fn decompressed_size(delivery_blob: &[u8]) -> Result<u64, DecompressError> {
51    let header = DeliveryBlobHeader::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
52    match header.delivery_type {
53        DeliveryBlobType::Type1 => Type1Blob::decompressed_size(delivery_blob),
54        _ => Err(DecompressError::DeliveryBlob(DeliveryBlobError::InvalidType)),
55    }
56}
57
58/// Returns the decompressed size of the delivery blob from `reader`.
59pub fn decompressed_size_from_reader(
60    mut reader: impl std::io::Read,
61) -> Result<u64, DecompressError> {
62    let mut buf = vec![];
63    loop {
64        let already_read = buf.len();
65        let new_size = already_read + 4096;
66        buf.resize(new_size, 0);
67        let new_size = already_read + reader.read(&mut buf[already_read..new_size])?;
68        if new_size == already_read {
69            return Err(DecompressError::NeedMoreData);
70        }
71        buf.truncate(new_size);
72        match decompressed_size(&buf) {
73            Ok(size) => {
74                return Ok(size);
75            }
76            Err(DecompressError::NeedMoreData) => {}
77            Err(e) => {
78                return Err(e);
79            }
80        }
81    }
82}
83
84/// Decompress a delivery blob in `delivery_blob`, delivery blob type is auto detected.
85pub fn decompress(delivery_blob: &[u8]) -> Result<Vec<u8>, DecompressError> {
86    let header = DeliveryBlobHeader::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
87    match header.delivery_type {
88        DeliveryBlobType::Type1 => Type1Blob::decompress(delivery_blob),
89        _ => Err(DecompressError::DeliveryBlob(DeliveryBlobError::InvalidType)),
90    }
91}
92
93/// Decompress a delivery blob in `delivery_blob`, and write the decompressed blob to `writer`,
94/// delivery blob type is auto detected.
95pub fn decompress_to(
96    delivery_blob: &[u8],
97    writer: impl std::io::Write,
98) -> Result<(), DecompressError> {
99    let header = DeliveryBlobHeader::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
100    match header.delivery_type {
101        DeliveryBlobType::Type1 => Type1Blob::decompress_to(delivery_blob, writer),
102        _ => Err(DecompressError::DeliveryBlob(DeliveryBlobError::InvalidType)),
103    }
104}
105
106/// Calculate the merkle root digest of the decompressed `delivery_blob`, delivery blob type is auto
107/// detected.
108pub fn calculate_digest(delivery_blob: &[u8]) -> Result<fuchsia_merkle::Hash, DecompressError> {
109    let mut writer = fuchsia_merkle::BufferedMerkleRootBuilder::default();
110    let header = DeliveryBlobHeader::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
111    match header.delivery_type {
112        DeliveryBlobType::Type1 => {
113            let () = Type1Blob::decompress_to(delivery_blob, &mut writer)?;
114        }
115        _ => return Err(DecompressError::DeliveryBlob(DeliveryBlobError::InvalidType)),
116    }
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 support the zstd-chunked compression format.
192    Type1 = 1,
193}
194
195impl TryFrom<u32> for DeliveryBlobType {
196    type Error = DeliveryBlobError;
197    fn try_from(value: u32) -> Result<Self, Self::Error> {
198        match value {
199            value if value == DeliveryBlobType::Reserved as u32 => Ok(DeliveryBlobType::Reserved),
200            value if value == DeliveryBlobType::Type1 as u32 => Ok(DeliveryBlobType::Type1),
201            _ => Err(DeliveryBlobError::InvalidType),
202        }
203    }
204}
205
206impl From<DeliveryBlobType> for u32 {
207    fn from(value: DeliveryBlobType) -> Self {
208        value as u32
209    }
210}
211
212/// Mode specifying when a delivery blob should be compressed.
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub enum CompressionMode {
215    /// Never compress input, output uncompressed.
216    Never,
217    /// Compress input, output compressed if saves space, otherwise uncompressed.
218    Attempt,
219    /// Compress input, output compressed unconditionally (even if space is wasted).
220    Always,
221}
222
223/// Header + metadata fields of a Type 1 blob.
224///
225/// **WARNING**: Outside of storage-owned components, this should only be used for informational
226/// or debugging purposes. The contents of this struct should be considered internal implementation
227/// details and are subject to change at any time.
228#[derive(Clone, Copy, Debug)]
229pub struct Type1Blob {
230    // Header:
231    pub header: DeliveryBlobHeader,
232    // Metadata:
233    pub payload_length: usize,
234    pub is_compressed: bool,
235}
236
237impl Type1Blob {
238    pub const HEADER: DeliveryBlobHeader = DeliveryBlobHeader {
239        delivery_type: DeliveryBlobType::Type1,
240        header_length: std::mem::size_of::<SerializedType1Blob>() as u32,
241    };
242
243    pub const CHUNKED_ARCHIVE_OPTIONS: ChunkedArchiveOptions = ChunkedArchiveOptions::V2 {
244        chunk_alignment: fuchsia_merkle::BLOCK_SIZE,
245        minimum_chunk_size: 32 * 1024,
246        compression_level: 14,
247    };
248
249    /// Generate a Type 1 delivery blob for `data` using the specified `mode`.
250    ///
251    /// **WARNING**: This function will panic on error.
252    // TODO(https://fxbug.dev/42073034): Bubble up library/compression errors.
253    pub fn generate(data: &[u8], mode: CompressionMode) -> Vec<u8> {
254        let mut delivery_blob: Vec<u8> = vec![];
255        Self::generate_to(data, mode, &mut delivery_blob).unwrap();
256        delivery_blob
257    }
258
259    /// Generate a Type 1 delivery blob for `data` using the specified `mode`. Writes delivery blob
260    /// directly into `writer`.
261    ///
262    /// **WARNING**: This function will panic on compression errors.
263    // TODO(https://fxbug.dev/42073034): Bubble up library/compression errors.
264    pub fn generate_to(
265        data: &[u8],
266        mode: CompressionMode,
267        mut writer: impl std::io::Write,
268    ) -> Result<(), std::io::Error> {
269        // Compress `data` depending on `compression_mode` and if we save any space.
270        let compressed = match mode {
271            CompressionMode::Attempt | CompressionMode::Always => {
272                let compressed = ChunkedArchive::new(data, Self::CHUNKED_ARCHIVE_OPTIONS)
273                    .expect("failed to compress data");
274                if mode == CompressionMode::Always || compressed.serialized_size() <= data.len() {
275                    Some(compressed)
276                } else {
277                    None
278                }
279            }
280            CompressionMode::Never => None,
281        };
282
283        // Write header to `writer`.
284        let payload_length =
285            compressed.as_ref().map(|archive| archive.serialized_size()).unwrap_or(data.len());
286        let header =
287            Self { header: Type1Blob::HEADER, payload_length, is_compressed: compressed.is_some() };
288        let serialized_header: SerializedType1Blob = header.into();
289        writer.write_all(serialized_header.as_bytes())?;
290
291        // Write payload to `writer`.
292        if let Some(archive) = compressed {
293            archive.write(writer)?;
294        } else {
295            writer.write_all(data)?;
296        }
297        Ok(())
298    }
299
300    /// Attempt to parse `data` as a Type 1 delivery blob. On success, returns validated blob info,
301    /// and the remainder of `data` representing the blob payload.
302    /// **WARNING**: This function does not verify that the payload is complete. Only the full
303    /// header and metadata portion of a delivery blob are required to be present in `data`.
304    pub fn parse(data: &[u8]) -> Result<Option<(Type1Blob, &[u8])>, DeliveryBlobError> {
305        let Ok((serialized_header, payload)) = Ref::<_, SerializedType1Blob>::from_prefix(data)
306        else {
307            return Ok(None);
308        };
309        serialized_header.decode().map(|metadata| Some((metadata, payload)))
310    }
311
312    /// Return the decompressed size of the blob without decompressing it.
313    pub fn decompressed_size(delivery_blob: &[u8]) -> Result<u64, DecompressError> {
314        let (header, payload) = Self::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
315        if !header.is_compressed {
316            return Ok(header.payload_length as u64);
317        }
318
319        let (decoded_archive, _chunk_data) =
320            compression::decode_archive(payload, header.payload_length)?
321                .ok_or(DecompressError::NeedMoreData)?;
322        Ok(decoded_archive.decompressed_size() as u64)
323    }
324
325    /// Decompress a Type 1 delivery blob in `delivery_blob`.
326    pub fn decompress(delivery_blob: &[u8]) -> Result<Vec<u8>, DecompressError> {
327        let mut decompressed = vec![];
328        decompressed.reserve(Self::decompressed_size(delivery_blob)? as usize);
329        Self::decompress_to(delivery_blob, &mut decompressed)?;
330        Ok(decompressed)
331    }
332
333    /// Decompress a Type 1 delivery blob in `delivery_blob` to `writer`.
334    pub fn decompress_to(
335        delivery_blob: &[u8],
336        mut writer: impl std::io::Write,
337    ) -> Result<(), DecompressError> {
338        let (header, payload) = Self::parse(delivery_blob)?.ok_or(DecompressError::NeedMoreData)?;
339        if !header.is_compressed {
340            return Ok(writer.write_all(payload)?);
341        }
342
343        let (decoded_archive, chunk_data) =
344            compression::decode_archive(payload, header.payload_length)?
345                .ok_or(DecompressError::NeedMoreData)?;
346        let mut decompressor = ChunkedDecompressor::new(decoded_archive)?;
347        let mut result = Ok(());
348        let mut chunk_callback = |chunk: &[u8]| {
349            if let Err(e) = writer.write_all(chunk) {
350                result = Err(e.into());
351            }
352        };
353        decompressor.update(chunk_data, &mut chunk_callback)?;
354        result
355    }
356}
357
358#[cfg(test)]
359mod tests {
360
361    use super::*;
362    use rand::Rng;
363
364    const DATA_LEN: usize = 500_000;
365
366    #[test]
367    fn compression_mode_never() {
368        let data: Vec<u8> = vec![0; DATA_LEN];
369        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Never);
370        // Payload should be uncompressed and have the same size as the original input data.
371        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
372        assert!(!header.is_compressed);
373        assert_eq!(header.payload_length, data.len());
374        assert_eq!(Type1Blob::decompress(&delivery_blob).unwrap(), data);
375    }
376
377    #[test]
378    fn compression_mode_always() {
379        let data: Vec<u8> = {
380            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
381            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
382        };
383        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
384        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
385        // Payload is not very compressible, so we expect it to be larger than the original.
386        assert!(header.is_compressed);
387        assert!(header.payload_length > data.len());
388        assert_eq!(Type1Blob::decompress(&delivery_blob).unwrap(), data);
389    }
390
391    #[test]
392    fn compression_mode_attempt_uncompressible() {
393        let data: Vec<u8> = {
394            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
395            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
396        };
397        // Data is random and therefore shouldn't be very compressible.
398        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Attempt);
399        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
400        assert!(!header.is_compressed);
401        assert_eq!(header.payload_length, data.len());
402        assert_eq!(Type1Blob::decompress(&delivery_blob).unwrap(), data);
403    }
404
405    #[test]
406    fn compression_mode_attempt_compressible() {
407        let data: Vec<u8> = vec![0; DATA_LEN];
408        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Attempt);
409        let (header, _) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
410        // Payload should be compressed and smaller than the original input.
411        assert!(header.is_compressed);
412        assert!(header.payload_length < data.len());
413        assert_eq!(Type1Blob::decompress(&delivery_blob).unwrap(), data);
414    }
415
416    #[test]
417    fn get_decompressed_size() {
418        let data: Vec<u8> = {
419            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
420            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
421        };
422        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
423        assert_eq!(decompressed_size(&delivery_blob).unwrap(), DATA_LEN as u64);
424        assert_eq!(decompressed_size_from_reader(&delivery_blob[..]).unwrap(), DATA_LEN as u64);
425    }
426
427    #[test]
428    fn test_calculate_digest() {
429        let data: Vec<u8> = {
430            let range = rand::distr::Uniform::<u8>::new_inclusive(0, 255).unwrap();
431            rand::rng().sample_iter(&range).take(DATA_LEN).collect()
432        };
433        let delivery_blob = Type1Blob::generate(&data, CompressionMode::Always);
434        assert_eq!(
435            calculate_digest(&delivery_blob).unwrap(),
436            fuchsia_merkle::root_from_slice(&data)
437        );
438    }
439}