Skip to main content

heddle_object_model/compact/
blob.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use super::{
4    Result,
5    io::{Reader, Writer},
6    limits::{MAX_COMPACT_COUNT, MIN_BLOB_ITEM_BYTES},
7};
8use crate::object::ContentHash;
9
10const BLOB_MAGIC: &[u8; 4] = b"HCB2";
11
12/// One verified blob slice within a lineage-solid frame.
13pub type DecodedBlob<'a> = (ContentHash, &'a [u8]);
14
15/// Whether `bytes` begin with the lineage-solid blob-frame discriminator.
16pub fn is_blob_frame(bytes: &[u8]) -> bool {
17    bytes.starts_with(BLOB_MAGIC)
18}
19
20/// Encode blob bodies newest-to-oldest in one checksummed solid frame.
21///
22/// Lengths precede the concatenated bodies, giving the frame reader an exact
23/// `(offset, len)` for every object while zstd sees one continuous lineage.
24pub fn encode_blob_frame(blobs: &[&[u8]]) -> Result<Vec<u8>> {
25    if blobs.len() > MAX_COMPACT_COUNT {
26        return Err(super::invalid(format!(
27            "blob frame count {} exceeds maximum {MAX_COMPACT_COUNT}",
28            blobs.len()
29        )));
30    }
31    let mut output = Writer::new(BLOB_MAGIC);
32    output.put_u64(blobs.len() as u64);
33    if let Some((first, rest)) = blobs.split_first() {
34        output.put_u64(first.len() as u64);
35        let mut previous = i64::try_from(first.len())
36            .map_err(|_| super::invalid("blob length exceeds signed delta range"))?;
37        for blob in rest {
38            let current = i64::try_from(blob.len())
39                .map_err(|_| super::invalid("blob length exceeds signed delta range"))?;
40            output.put_i64(current - previous);
41            previous = current;
42        }
43    }
44    for blob in blobs {
45        output.put_fixed(blob);
46    }
47    Ok(output.finish())
48}
49
50/// Decode and whole-frame-verify every indexed blob slice.
51pub fn decode_blob_frame(bytes: &[u8]) -> Result<Vec<DecodedBlob<'_>>> {
52    let mut input = Reader::verified(bytes, BLOB_MAGIC)?;
53    let count = input.get_count("blob frame", MIN_BLOB_ITEM_BYTES)?;
54    let mut lengths = Vec::with_capacity(count);
55    if count > 0 {
56        let first = input.get_u64()?;
57        lengths.push(checked_length(first)?);
58        let mut previous = i64::try_from(first)
59            .map_err(|_| super::invalid("blob length exceeds signed delta range"))?;
60        for _ in 1..count {
61            let current = previous
62                .checked_add(input.get_i64()?)
63                .ok_or_else(|| super::invalid("blob length delta overflow"))?;
64            if current < 0 {
65                return Err(super::invalid("blob length delta became negative"));
66            }
67            lengths.push(checked_length(current as u64)?);
68            previous = current;
69        }
70    }
71    let minimum_remaining = lengths
72        .iter()
73        .try_fold(0usize, |total, len| total.checked_add(*len))
74        .ok_or_else(|| super::invalid("blob frame length overflow"))?;
75    if input.remaining() != minimum_remaining {
76        return Err(super::invalid(format!(
77            "blob lengths total {minimum_remaining}, frame has {} body bytes",
78            input.remaining()
79        )));
80    }
81    let mut blobs = Vec::with_capacity(count);
82    for len in lengths {
83        let body = input.take(len)?;
84        blobs.push((ContentHash::compute_typed("blob", body), body));
85    }
86    input.finish()?;
87    Ok(blobs)
88}
89
90fn checked_length(value: u64) -> Result<usize> {
91    usize::try_from(value).map_err(|_| super::invalid("blob length exceeds platform limits"))
92}