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