Skip to main content

vole_document/materialize/
seek.rs

1//! Seek-based partial materialization (Phase 8.2).
2//!
3//! [`materialize_observation_seeked`] serves the same narrow byte range as
4//! [`crate::materialize::observation::materialize_observation`], but from a
5//! `Read + Seek` source and by reading **only the records the query needs**. It
6//! does this by reading the optional `DIRECTORY` record (written as the first
7//! record at offset 64), which locates every record class, and then seeking
8//! directly to the `GRAPH`, `OBSERVATION_INDEX`, `INTEGRITY`, and the referenced
9//! `OBJECT`/`ENTROPY_CHANNEL`/`MODEL` records.
10//!
11//! ## Decline, never guess
12//!
13//! A descriptor whose header does not advertise the seek feature, or whose first
14//! record is not a `DIRECTORY`, is declined with
15//! [`crate::ErrorClass::UnsupportedFeature`]; the reader never silently falls
16//! back to reading the whole file. Op selection, lazy channel decoding, and the
17//! evaluated slice are shared verbatim with the Phase-7 in-memory path
18//! ([`crate::materialize::observation::select_ops`] / `serve_selection`), so the
19//! two readers cannot diverge.
20//!
21//! ## The directory is advisory, never authority
22//!
23//! Every locator is cross-checked against the record it points at (tag byte and
24//! payload length), every record's own CRC32C is verified when it is read, the
25//! directory's internal geometry is validated by
26//! [`SeekDirectory::validate_structural`], and the `ObservationIndex` is
27//! re-derived against the directory-derived object/channel lengths with
28//! [`ObservationIndex::validate`]. A lying directory is rejected, never trusted.
29//!
30//! ## Integrity is honest
31//!
32//! A partial read cannot recompute the whole-source SHA-256, so a served slice is
33//! an *observation* consistent with the descriptor's own validated
34//! program/index/directory — **not** a verified archival read. The returned
35//! [`ObservationStats`] carries `integrity_verified == false` and a real
36//! `bytes_read` measured by the internal [`CountingReader`]. Only
37//! `materialize`/`decode`/`verify` check `INTEGRITY` and are the archival
38//! authority.
39
40use std::io::{self, Read, Seek, SeekFrom};
41
42use crate::container::checkpoint::CheckpointTable;
43use crate::container::directory::{
44    DirectoryEntry, SECTION_CHANNEL_LENGTHS, SECTION_LOCATORS, SeekDirectory,
45};
46use crate::container::header::{FEATURE_SEEK_DIRECTORY, HEADER_LEN, Header};
47use crate::container::observation::ObservationIndex;
48use crate::container::record::{FLAG_OPTIONAL, RECORD_OVERHEAD, Record, RecordTag, read_record_at};
49use crate::dra::Program;
50use crate::entropy::codec::EntropyChannelDescriptor;
51use crate::entropy::model::EntropyModel;
52use crate::entropy::{CODER_ORDER0_BYTE_RANS, CODER_VERSION_1};
53use crate::error::{Error, Result};
54use crate::limits::Limits;
55use crate::materialize::observation::{
56    ObservationReport, ObservationSelector, ObservationStats, OpWindow, resolve_byte_range,
57    resolve_selector, select_ops, select_ops_from_lengths, selection_references, serve_selection,
58};
59
60/// A `Read + Seek` wrapper that counts the bytes actually returned by `read` and
61/// the number of `read`/`seek` calls.
62///
63/// This is the primary instrument for the Phase-8 bytes-read claim: it is exact,
64/// deterministic, attributes bytes to VOLE (not to a loader or pipe read-ahead),
65/// and is immune to page-cache effects. The library wraps one internally in
66/// [`materialize_observation_seeked`] and reports `bytes_read`; it is exposed so
67/// callers and tests can measure I/O directly.
68#[derive(Debug)]
69pub struct CountingReader<R> {
70    inner: R,
71    bytes_read: u64,
72    read_calls: u32,
73    seeks: u32,
74}
75
76impl<R> CountingReader<R> {
77    /// Wrap `inner`, starting all counters at zero.
78    pub fn new(inner: R) -> Self {
79        CountingReader {
80            inner,
81            bytes_read: 0,
82            read_calls: 0,
83            seeks: 0,
84        }
85    }
86
87    /// Total payload bytes returned by `read` so far.
88    pub fn bytes_read(&self) -> u64 {
89        self.bytes_read
90    }
91
92    /// Number of `read` calls (each may return fewer bytes than requested).
93    pub fn read_calls(&self) -> u32 {
94        self.read_calls
95    }
96
97    /// Number of `seek` calls.
98    pub fn seeks(&self) -> u32 {
99        self.seeks
100    }
101
102    /// Unwrap the inner reader.
103    pub fn into_inner(self) -> R {
104        self.inner
105    }
106}
107
108impl<R: Read> Read for CountingReader<R> {
109    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
110        let n = self.inner.read(buf)?;
111        self.bytes_read += n as u64;
112        self.read_calls += 1;
113        Ok(n)
114    }
115}
116
117impl<R: Seek> Seek for CountingReader<R> {
118    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
119        self.seeks += 1;
120        self.inner.seek(pos)
121    }
122}
123
124/// The selection a lane yields: the resolved output range `[a, b)`, the op
125/// window to evaluate, and the referenced-object/channel flags.
126type Selection = (u64, u64, OpWindow, Vec<bool>, Vec<bool>);
127
128/// Serve one observation from a seekable `.voldoc` source by reading only the
129/// records the query needs.
130///
131/// The returned bytes equal `materialize(parsed)[a..b]` for the resolved range.
132/// A source without a seek directory is declined (never silently fully read).
133/// See the module documentation for the validation and integrity rules.
134pub fn materialize_observation_seeked<R: Read + Seek>(
135    reader: R,
136    selector: ObservationSelector,
137    limits: Limits,
138) -> Result<ObservationReport> {
139    let mut reader = CountingReader::new(reader);
140    let file_len = reader
141        .seek(SeekFrom::End(0))
142        .map_err(|e| Error::io(format!("seek to end failed: {e}")))?;
143
144    // (a) Header, then the fixed-offset DIRECTORY record.
145    let header = read_header(&mut reader)?;
146    if header.optional_features & FEATURE_SEEK_DIRECTORY == 0 {
147        return Err(Error::unsupported_feature(
148            "descriptor has no seek directory (the header does not declare the seek feature)",
149        ));
150    }
151    let dir_rec = read_record_at(&mut reader, HEADER_LEN as u64, limits)?;
152    if dir_rec.tag != RecordTag::Directory as u8 {
153        return Err(Error::unsupported_feature(
154            "descriptor has no seek directory record at offset 64",
155        ));
156    }
157    if dir_rec.flags & FLAG_OPTIONAL == 0 {
158        return Err(Error::invalid_container(
159            "DIRECTORY record must carry FLAG_OPTIONAL",
160        ));
161    }
162    let dir = SeekDirectory::decode(&dir_rec.payload, limits)?;
163    dir.validate_structural(file_len, limits)?;
164    if dir.section_flags & SECTION_LOCATORS == 0 {
165        return Err(Error::unsupported_feature(
166            "seek directory omits the locator section",
167        ));
168    }
169    if dir.entries[0].payload_len != dir_rec.payload.len() as u32 {
170        return Err(Error::invalid_container(
171            "seek directory locator 0 payload length disagrees with the record framing",
172        ));
173    }
174
175    let object_entries = class_entries(&dir, RecordTag::Object)?;
176    let channel_entries = class_entries(&dir, RecordTag::EntropyChannel)?;
177    let model_entries = class_entries(&dir, RecordTag::Model)?;
178    if !class_entries(&dir, RecordTag::ExternalRef)?.is_empty() {
179        return Err(Error::unsupported_feature(
180            "seek-based partial read cannot resolve external objects",
181        ));
182    }
183    if !channel_entries.is_empty() && dir.section_flags & SECTION_CHANNEL_LENGTHS == 0 {
184        return Err(Error::unsupported_feature(
185            "seek directory omits the channel-lengths section",
186        ));
187    }
188    if dir.channel_lengths.len() != channel_entries.len() {
189        return Err(Error::invalid_container(
190            "seek directory channel-length table disagrees with the channel locators",
191        ));
192    }
193    let object_lens: Vec<u64> = object_entries
194        .iter()
195        .map(|e| u64::from(e.payload_len))
196        .collect();
197    let channel_lens: Vec<u64> = dir.channel_lengths.clone();
198
199    // (b) GRAPH and INTEGRITY are needed by every lane: the program is the
200    // authority for op selection and for validating any checkpoint, and INTEGRITY
201    // supplies the declared source length.
202    let graph_site = class_entries(&dir, RecordTag::Graph)?
203        .first()
204        .ok_or_else(|| {
205            Error::unsupported_feature("seek directory does not locate a GRAPH record")
206        })?;
207    let integrity_site = class_entries(&dir, RecordTag::Integrity)?
208        .first()
209        .ok_or_else(|| {
210            Error::unsupported_feature("seek directory does not locate an INTEGRITY record")
211        })?;
212    let checkpoint_sites = class_entries(&dir, RecordTag::Checkpoint)?;
213
214    let graph_rec = read_checked(&mut reader, graph_site, limits)?;
215    let program = Program::decode(&graph_rec.payload, limits)?;
216    let integrity_rec = read_checked(&mut reader, integrity_site, limits)?;
217    if integrity_rec.payload.len() != 40 {
218        return Err(Error::invalid_container(
219            "INTEGRITY payload must be 40 bytes",
220        ));
221    }
222    let declared_len = u64::from_le_bytes([
223        integrity_rec.payload[32],
224        integrity_rec.payload[33],
225        integrity_rec.payload[34],
226        integrity_rec.payload[35],
227        integrity_rec.payload[36],
228        integrity_rec.payload[37],
229        integrity_rec.payload[38],
230        integrity_rec.payload[39],
231    ]);
232    if declared_len != header.declared_source_len {
233        return Err(Error::integrity_mismatch(format!(
234            "INTEGRITY length {declared_len} disagrees with header {}",
235            header.declared_source_len
236        )));
237    }
238
239    // (c) Selection. Two lanes produce the same `(range, op window, dependency
240    // set)`:
241    //   * the Phase-8 index lane reads and re-derives the OBSERVATION_INDEX;
242    //   * the checkpoint lane (Phase 13.4), usable only for a raw byte range
243    //     (the one selector that needs no index), consumes a *validated*
244    //     CHECKPOINT and reads no index record.
245    // The checkpoint is advisory: a missing, corrupt, non-optional, or lying one
246    // is ignored and the reader falls back to the index lane -- never to a
247    // guess -- so a checkpoint can neither change a served byte nor deny service.
248    let mut checkpoint_bytes: u64 = 0;
249    let mut index_bytes: u64 = 0;
250    let (a, b, window, objects_used, channels_used) = {
251        let mut via_checkpoint: Option<Selection> = None;
252        if matches!(selector, ObservationSelector::ByteRange { .. })
253            && let Some(site) = checkpoint_sites.first()
254            && let Ok(cp_rec) = read_checked(&mut reader, site, limits)
255            && cp_rec.is_optional()
256            && let Ok(cp) = CheckpointTable::decode(&cp_rec.payload, limits)
257            && cp
258                .validate(
259                    &program,
260                    &graph_rec.payload,
261                    declared_len,
262                    &object_lens,
263                    &channel_lens,
264                    limits,
265                )
266                .is_ok()
267        {
268            let (ra, rb) = resolve_byte_range(selector, declared_len)?;
269            let w = select_ops_from_lengths(&program, &cp.lengths(), ra, rb)?;
270            let (ou, cu) =
271                selection_references(&w.ops, object_entries.len(), channel_entries.len());
272            checkpoint_bytes = cp_rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
273            via_checkpoint = Some((ra, rb, w, ou, cu));
274        }
275        match via_checkpoint {
276            Some((ra, rb, w, ou, cu)) => (ra, rb, w, ou, cu),
277            None => {
278                let index_site = class_entries(&dir, RecordTag::ObservationIndex)?
279                    .first()
280                    .ok_or_else(|| {
281                        Error::unsupported_feature(
282                            "seek directory does not locate an OBSERVATION_INDEX record",
283                        )
284                    })?;
285                let index_rec = read_checked(&mut reader, index_site, limits)?;
286                let index = ObservationIndex::decode(&index_rec.payload, limits)?;
287                index.validate(&program, &object_lens, &channel_lens, limits)?;
288                index_bytes = index_rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
289                let (ra, rb) = resolve_selector(&index, selector, declared_len)?;
290                let w = select_ops(&program, &object_lens, &channel_lens, ra, rb, limits)?;
291                let (ou, cu) =
292                    selection_references(&w.ops, object_entries.len(), channel_entries.len());
293                (ra, rb, w, ou, cu)
294            }
295        }
296    };
297
298    // (d) Read ONLY the referenced OBJECT records.
299    let mut objects: Vec<Vec<u8>> = vec![Vec::new(); object_entries.len()];
300    for (id, used) in objects_used.iter().enumerate() {
301        if *used {
302            objects[id] = read_checked(&mut reader, &object_entries[id], limits)?.payload;
303        }
304    }
305
306    // (d) Read ONLY the referenced ENTROPY_CHANNEL records, then the MODEL records
307    // they name.
308    let mut channels: Vec<EntropyChannelDescriptor> =
309        vec![placeholder_channel(); channel_entries.len()];
310    let mut models_needed = vec![false; model_entries.len()];
311    for (id, used) in channels_used.iter().enumerate() {
312        if *used {
313            let rec = read_checked(&mut reader, &channel_entries[id], limits)?;
314            let channel = EntropyChannelDescriptor::decode(&rec.payload, limits)?;
315            if channel.decoded_length != channel_lens[id] {
316                return Err(Error::invalid_container(format!(
317                    "seek directory channel-length {id} disagrees with the channel record"
318                )));
319            }
320            if channel.model_id as usize >= model_entries.len() {
321                return Err(Error::invalid_model(format!(
322                    "entropy channel {id} references missing model {}",
323                    channel.model_id
324                )));
325            }
326            models_needed[channel.model_id as usize] = true;
327            channels[id] = channel;
328        }
329    }
330    let mut models: Vec<EntropyModel> = vec![placeholder_model(); model_entries.len()];
331    for (id, used) in models_needed.iter().enumerate() {
332        if *used {
333            let rec = read_checked(&mut reader, &model_entries[id], limits)?;
334            models[id] = EntropyModel::decode(&rec.payload)?;
335        }
336    }
337
338    // (e) Evaluate the selected ops and slice `[a, b)` exactly as Phase 7 does.
339    let served = serve_selection(
340        &objects,
341        &channels,
342        &models,
343        window,
344        &objects_used,
345        &channels_used,
346        a,
347        b,
348        limits,
349    )?;
350
351    // `descriptor_bytes_traversed` stays comparable with the Phase-7 path: graph
352    // payload + the selection lane's own record (index **or** checkpoint) + the
353    // referenced object/channel payloads.
354    let descriptor_bytes_traversed = graph_rec.payload.len() as u64
355        + index_bytes
356        + checkpoint_bytes
357        + served.referenced_object_bytes
358        + served.referenced_channel_bytes;
359
360    let stats = ObservationStats {
361        ops_evaluated: served.ops_evaluated,
362        ops_total: served.ops_total,
363        objects_fetched: served.objects_fetched,
364        objects_total: object_entries.len(),
365        channels_decoded: served.channels_decoded,
366        channels_total: channel_entries.len(),
367        entropy_bytes_decoded: served.entropy_bytes_decoded,
368        descriptor_bytes_traversed,
369        output_bytes: served.bytes.len() as u64,
370        // (f) Real I/O, distinct from the Phase-7 CPU-side approximation.
371        bytes_read: reader.bytes_read(),
372        integrity_verified: false,
373    };
374
375    Ok(ObservationReport {
376        range: (a, b),
377        bytes: served.bytes,
378        stats,
379    })
380}
381
382/// Read and decode the fixed 64-byte header from the start of the source.
383fn read_header<R: Read + Seek>(reader: &mut R) -> Result<Header> {
384    reader
385        .seek(SeekFrom::Start(0))
386        .map_err(|e| Error::io(format!("seek to header failed: {e}")))?;
387    let mut buf = [0u8; HEADER_LEN];
388    reader
389        .read_exact(&mut buf)
390        .map_err(|_| Error::invalid_container("truncated header"))?;
391    Header::decode(&buf)
392}
393
394/// Read the record at `site` and require its framing to match the locator.
395fn read_checked<R: Read + Seek>(
396    reader: &mut R,
397    site: &DirectoryEntry,
398    limits: Limits,
399) -> Result<Record> {
400    let rec = read_record_at(reader, site.offset, limits)?;
401    if rec.tag != site.tag || rec.payload.len() as u64 != u64::from(site.payload_len) {
402        return Err(Error::invalid_container(format!(
403            "DIRECTORY locator for tag {:#04x} disagrees with the record framing at offset {}",
404            site.tag, site.offset
405        )));
406    }
407    Ok(rec)
408}
409
410/// The locators of `tag`, using the directory's `CLASS_INDEX` and re-checking it
411/// against a linear scan of the locator table (defence in depth;
412/// [`SeekDirectory::validate_structural`] has already required agreement).
413fn class_entries(dir: &SeekDirectory, tag: RecordTag) -> Result<&[DirectoryEntry]> {
414    let (first, count) = match dir.classes.iter().find(|c| c.tag == tag as u8) {
415        Some(c) => (c.first as usize, c.count as usize),
416        None => (0, 0),
417    };
418    let scan_first = dir.entries.iter().position(|e| e.tag == tag as u8);
419    let scan_count = dir.entries.iter().filter(|e| e.tag == tag as u8).count();
420    let expected_first = if count == 0 { None } else { Some(first) };
421    if scan_first != expected_first || scan_count != count {
422        return Err(Error::invalid_container(
423            "seek directory class index disagrees with the locator table",
424        ));
425    }
426    if count == 0 {
427        return Ok(&[]);
428    }
429    let end = first
430        .checked_add(count)
431        .ok_or_else(|| Error::invalid_container("seek directory class range overflow"))?;
432    dir.entries
433        .get(first..end)
434        .ok_or_else(|| Error::invalid_container("seek directory class range out of bounds"))
435}
436
437/// A never-dereferenced placeholder occupying an unreferenced channel slot, so
438/// index positions in the partial vectors stay aligned with the descriptor's
439/// own tables.
440fn placeholder_channel() -> EntropyChannelDescriptor {
441    EntropyChannelDescriptor {
442        coder: CODER_ORDER0_BYTE_RANS,
443        coder_version: CODER_VERSION_1,
444        scale_bits: 0,
445        lane_count: 1,
446        model_id: 0,
447        symbol_count: 0,
448        decoded_length: 0,
449        initial_state: 0,
450        payload: Vec::new(),
451    }
452}
453
454/// A never-dereferenced placeholder occupying an unreferenced model slot.
455fn placeholder_model() -> EntropyModel {
456    EntropyModel {
457        scale_bits: 0,
458        frequencies: Vec::new(),
459    }
460}