Skip to main content

urna_runtime/
blobs.rs

1//! Blob section open logic (0x14 blob_refs + 0x16 blob_span_overlay),
2//! carved out of `mmap_file.rs` so that file stays under the 300-line
3//! crate guard. Both sections are OPTIONAL and content_hash-excluded, and
4//! open only behind the additive `blobs_present` capability, mirroring how
5//! the graph (0x0C) opens behind `graph_present`.
6
7use urna_format::UrnaError;
8use urna_format::layout::{
9    SECTION_BLOB_DATA, SECTION_BLOB_REFS, SECTION_BLOB_SPAN_OVERLAY, SECTION_ENCODING_RAW,
10};
11use urna_format::reader::UrnaView;
12use urna_format::sections::{
13    BLOB_REF_NONE, BlobRefRecord, OriginalSpan, decode_blob_data_table, decode_blob_refs,
14    decode_blob_span_overlay,
15};
16
17use crate::error::RuntimeError;
18
19/// Opened 0x17 blob_data: the offset table plus the ABSOLUTE file offset
20/// of the first data byte, so `MmapUrnaFile::blob_bytes` can slice one
21/// blob lazily off the mmap without ever copying the section.
22#[derive(Clone, Debug)]
23pub struct OpenBlobData {
24    pub entries: Vec<(u64, u64)>,
25    pub abs_data_start: usize,
26}
27
28/// Open the 0x17 blob_data offset table when the section is present.
29/// The table must be RAW (never compressed — lazy slicing depends on it)
30/// and must parallel the 0x14 record order, so a length mismatch against
31/// `n_refs` is a typed format error.
32pub(crate) fn open_blob_data(
33    view: &UrnaView,
34    n_refs: usize,
35) -> Result<Option<OpenBlobData>, RuntimeError> {
36    let Some(entry) = view
37        .section_table
38        .iter()
39        .find(|e| e.section_id == SECTION_BLOB_DATA)
40    else {
41        return Ok(None);
42    };
43    if entry.encoding != SECTION_ENCODING_RAW {
44        return Err(RuntimeError::Format(UrnaError::MalformedSectionPayload {
45            section_id: SECTION_BLOB_DATA,
46            reason: format!("blob_data must be raw, found encoding {}", entry.encoding),
47        }));
48    }
49    let payload = view.get_section_data(SECTION_BLOB_DATA)?;
50    let table = decode_blob_data_table(payload)?;
51    if table.entries.len() != n_refs {
52        return Err(RuntimeError::Format(UrnaError::MalformedSectionPayload {
53            section_id: SECTION_BLOB_DATA,
54            reason: format!(
55                "blob_data has {} entries but blob_refs has {} records",
56                table.entries.len(),
57                n_refs
58            ),
59        }));
60    }
61    Ok(Some(OpenBlobData {
62        entries: table.entries,
63        abs_data_start: entry.offset as usize + table.data_start,
64    }))
65}
66
67impl crate::mmap_file::MmapUrnaFile {
68    /// Whether this file carries its media bytes inline (0x17 present).
69    pub fn has_blob_data(&self) -> bool {
70        self.blob_data.is_some()
71    }
72
73    /// Slice blob `index`'s bytes lazily off the mmap. Errors typed: no
74    /// 0x17 section, an out-of-range index, or a record that is not
75    /// inlined (its table entry is the (0, 0) gap).
76    pub fn blob_bytes(&self, index: usize) -> Result<&[u8], RuntimeError> {
77        let data = self
78            .blob_data
79            .as_ref()
80            .ok_or(RuntimeError::BlobNotInlined { index })?;
81        let &(offset, len) = data
82            .entries
83            .get(index)
84            .ok_or(RuntimeError::BlobNotInlined { index })?;
85        let inlined = self
86            .blob_refs
87            .as_ref()
88            .and_then(|refs| refs.get(index))
89            .is_some_and(|r| r.inlined);
90        if !inlined {
91            return Err(RuntimeError::BlobNotInlined { index });
92        }
93        let start = data.abs_data_start + offset as usize;
94        let end = start + len as usize;
95        // decode_blob_data_table bounded every entry against the section;
96        // this guards the absolute math against the physical mmap too.
97        self._mmap.get(start..end).ok_or_else(|| {
98            RuntimeError::Format(UrnaError::MalformedSectionPayload {
99                section_id: SECTION_BLOB_DATA,
100                reason: format!("blob {} spans past the file", index),
101            })
102        })
103    }
104}
105
106/// Open the blob pair. Returns the decoded 0x14 table (`None` when the
107/// capability or section is absent). When the 0x16 overlay is present,
108/// per-chunk spans that point into a blob REPLACE the decoded 0x03 spans
109/// in place (for a media corpus those carry ordinal placeholders), so
110/// cite/retrieve report the real blob-relative byte range against the
111/// blob's uri; BLOB_REF_NONE entries keep the 0x03 span (legacy/text
112/// chunks). a dangling blob_ref_index is a typed format error, never a
113/// silent fallback.
114pub(crate) fn open_blob_sections(
115    view: &UrnaView,
116    spans: &mut [OriginalSpan],
117) -> Result<Option<Vec<BlobRefRecord>>, RuntimeError> {
118    let blobs_present = view
119        .manifest
120        .capabilities_ext
121        .as_ref()
122        .and_then(|e| e.blobs_present)
123        .unwrap_or(false);
124    if !blobs_present {
125        return Ok(None);
126    }
127    let has = |id: u32| view.section_table.iter().any(|e| e.section_id == id);
128    let blob_refs = if has(SECTION_BLOB_REFS) {
129        let bytes = view.decoded_section(SECTION_BLOB_REFS)?;
130        Some(decode_blob_refs(&bytes)?)
131    } else {
132        None
133    };
134    if has(SECTION_BLOB_SPAN_OVERLAY) {
135        let bytes = view.decoded_section(SECTION_BLOB_SPAN_OVERLAY)?;
136        let overlay = decode_blob_span_overlay(&bytes)?;
137        for (i, entry) in overlay.iter().enumerate().take(spans.len()) {
138            if entry.blob_ref_index == BLOB_REF_NONE {
139                continue;
140            }
141            let uri = blob_refs
142                .as_ref()
143                .and_then(|refs| refs.get(entry.blob_ref_index as usize))
144                .map(|r| r.original_uri.clone())
145                .ok_or_else(|| {
146                    RuntimeError::Format(UrnaError::MalformedSectionPayload {
147                        section_id: SECTION_BLOB_SPAN_OVERLAY,
148                        reason: format!(
149                            "overlay entry {} references missing blob_ref {}",
150                            i, entry.blob_ref_index
151                        ),
152                    })
153                })?;
154            spans[i] = OriginalSpan {
155                source_uri: uri,
156                byte_start: entry.byte_start,
157                byte_end: entry.byte_end,
158            };
159        }
160    }
161    Ok(blob_refs)
162}