Skip to main content

urna_format/sections/blob/
span_overlay.rs

1//! blob_span_overlay (0x16): per-chunk overlay that replaces the canonical
2//! chunks_original_spans (0x03) for out-of-line media (images, video, audio).
3//!
4//! OPTIONAL and EXCLUDED from content_hash. When present, the runtime uses
5//! these spans for `cite` and `retrieve` instead of the spans in 0x03, so
6//! a corpus whose chunks index into an AV1 stream or external catalog keeps
7//! the SAME content_hash as its text-only twin.
8//!
9//! wire encoding: `raw` (self-describing payload, no compression).
10//! all integers le.
11
12use crate::bytes::{le_u32, le_u64};
13use crate::error::UrnaError;
14use crate::layout::SECTION_BLOB_SPAN_OVERLAY;
15
16pub const BLOB_SPAN_OVERLAY_PAYLOAD_VERSION: u32 = 1;
17pub const BLOB_REF_NONE: u32 = 0xFFFF_FFFF;
18
19/// one entry per chunk. `blob_ref_index` points into the 0x14 blob_refs
20/// table (or BLOB_REF_NONE for legacy/text chunks). `byte_start`/`byte_end`
21/// are the span inside that blob (or ordinal placeholders for backward
22/// compatibility with the current python image builder).
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct BlobSpanEntry {
25    pub blob_ref_index: u32,
26    pub byte_start: u64,
27    pub byte_end: u64,
28}
29
30fn malformed(reason: impl Into<String>) -> UrnaError {
31    UrnaError::MalformedSectionPayload {
32        section_id: SECTION_BLOB_SPAN_OVERLAY,
33        reason: reason.into(),
34    }
35}
36
37/// encode `entries` into the 0x16 payload. deterministic: same entries,
38/// same bytes.
39pub fn encode_blob_span_overlay(entries: &[BlobSpanEntry]) -> Result<Vec<u8>, UrnaError> {
40    let mut out = Vec::with_capacity(12 + entries.len() * 16);
41    out.extend_from_slice(&BLOB_SPAN_OVERLAY_PAYLOAD_VERSION.to_le_bytes());
42    out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
43    for e in entries {
44        out.extend_from_slice(&e.blob_ref_index.to_le_bytes());
45        out.extend_from_slice(&e.byte_start.to_le_bytes());
46        out.extend_from_slice(&e.byte_end.to_le_bytes());
47    }
48    Ok(out)
49}
50
51/// decode the payload back to entries. typed errors on truncation or version
52/// mismatch; never panics.
53pub fn decode_blob_span_overlay(bytes: &[u8]) -> Result<Vec<BlobSpanEntry>, UrnaError> {
54    let mut cur = Cursor::new(bytes);
55    let version = cur.u32()?;
56    if version != BLOB_SPAN_OVERLAY_PAYLOAD_VERSION {
57        return Err(UrnaError::UnsupportedSectionVersion {
58            section_id: SECTION_BLOB_SPAN_OVERLAY,
59            version,
60        });
61    }
62    let n = cur.u64()? as usize;
63    // bound the claim against the physical payload before allocating:
64    // every entry costs exactly 16 bytes (u32 + u64 + u64).
65    if n > (bytes.len() - cur.pos) / 16 {
66        return Err(malformed("blob_span_overlay: entry count exceeds payload"));
67    }
68    let mut entries = Vec::with_capacity(n);
69    for _ in 0..n {
70        entries.push(BlobSpanEntry {
71            blob_ref_index: cur.u32()?,
72            byte_start: cur.u64()?,
73            byte_end: cur.u64()?,
74        });
75    }
76    if cur.pos != bytes.len() {
77        return Err(malformed("trailing bytes after entries"));
78    }
79    Ok(entries)
80}
81
82struct Cursor<'a> {
83    buf: &'a [u8],
84    pos: usize,
85}
86
87impl<'a> Cursor<'a> {
88    fn new(buf: &'a [u8]) -> Self {
89        Self { buf, pos: 0 }
90    }
91    fn take(&mut self, n: usize) -> Result<&'a [u8], UrnaError> {
92        if n > self.buf.len() - self.pos {
93            return Err(malformed("unexpected EOF"));
94        }
95        let s = &self.buf[self.pos..self.pos + n];
96        self.pos += n;
97        Ok(s)
98    }
99    fn u32(&mut self) -> Result<u32, UrnaError> {
100        le_u32(self.take(4)?)
101    }
102    fn u64(&mut self) -> Result<u64, UrnaError> {
103        le_u64(self.take(8)?)
104    }
105}