Skip to main content

vole_document/materialize/
observation.rs

1//! Partial materialization / observation views (Phase 7.3).
2//!
3//! [`materialize_observation`] serves a narrow byte range of the reconstructed
4//! source from a parsed descriptor that carries an observation index. It is a
5//! *view* API: the complete-materialization path
6//! ([`crate::materialize::materialize`]) is unchanged and remains the authority
7//! for whole-source exactness.
8//!
9//! ## Decline, never guess
10//!
11//! A descriptor without an index is declined with
12//! `ErrorClass::UnsupportedFeature`; the function never silently falls back to
13//! full materialization, so a reported cost is never a whole-file cost in
14//! disguise.
15//!
16//! ## Op selection: linear skipping vs. a bounded prefix
17//!
18//! The op layout is re-derived from the program with
19//! [`Program::analyze_ops`] (the index's op table is advisory; the program is
20//! authoritative). Let the requested output range be `[a, b)`.
21//!
22//! * If every op is a *linear, independent block* producer — one of
23//!   [`Op::EmitObject`], [`Op::Inline`], [`Op::DecodeChannel`],
24//!   [`Op::DeflateReplay`], or [`Op::InterleaveChannels`] — then each op's output
25//!   is a pure function of its own inputs and its absolute start offset. Only the
26//!   ops whose output intersects `[a, b)` are evaluated; ops entirely before `a`
27//!   are skipped (no bytes produced, no channel decoded).
28//! * Otherwise (the program uses any position-dependent op: [`Op::RepeatLast`],
29//!   [`Op::MarkOffset`], [`Op::EmitOffset`], [`Op::PackSegments`], or
30//!   [`Op::PackedChannels`]) the ops `0..=last` are evaluated as a prefix. This
31//!   is correct but can produce up to the whole document; it is the honest
32//!   fallback. [`ObservationStats::work_amplification`] reports how much of the
33//!   program was walked.
34//!
35//! ## Lazy entropy-channel decoding
36//!
37//! Only the entropy channels referenced by the evaluated ops are decoded; the
38//! complete path in [`crate::materialize`] still decodes every channel. A
39//! referenced channel is decoded in full, so the conversion-contract tail check
40//! is preserved for exactly the channels that are used. Objects are already
41//! resident in the descriptor, so "fetched" here means "read by an evaluated op".
42//!
43//! ## `descriptor_bytes_traversed` is an approximation
44//!
45//! This in-memory path parses the whole framed descriptor into memory, so it has
46//! no true I/O seek accounting. [`ObservationStats::descriptor_bytes_traversed`]
47//! reports the sum of the serialized record payload lengths the path *needed* —
48//! the graph record, the referenced object payloads, the referenced channel
49//! payloads, and the index record — as a documented CPU-side approximation, not
50//! a byte-read figure. The referenced channel payloads are already included here,
51//! so [`ObservationStats::entropy_bytes_decoded`] is a **subset** of
52//! `descriptor_bytes_traversed` and the two fields must never be summed. This
53//! path reports `bytes_read == 0` (it performs no I/O of its own); the
54//! seek reader ([`crate::materialize::seek::materialize_observation_seeked`],
55//! Phase 8) reports a real, instrumented `bytes_read` while sharing this path's
56//! op selection and evaluation verbatim.
57//!
58//! A partial `view` — in-memory or seeked — is an *observation*: it serves bytes
59//! consistent with the descriptor's own validated program/index/directory, but it
60//! never recomputes the whole-source SHA-256, so `integrity_verified` is `false`.
61//! Only `materialize`/`decode`/`verify` are the archival authority.
62
63use crate::container::ParsedDescriptor;
64use crate::container::observation::{
65    ObservationIndex, ObservationSelector as IndexSelector, SECTION_PDF_SELECTORS, SELECTOR_OBJECT,
66    SELECTOR_REVISION, SELECTOR_STREAM,
67};
68use crate::dra::{Op, Program};
69use crate::entropy::codec::EntropyChannelDescriptor;
70use crate::entropy::model::EntropyModel;
71use crate::error::{Error, Result};
72use crate::limits::Limits;
73
74#[cfg(feature = "rans")]
75use crate::entropy::rans::{Capsule, decode_channel};
76
77/// A narrow observation of the reconstructed source.
78///
79/// Every selector resolves to a half-open output range `[a, b)` in the source
80/// address space (output offset `O` is source offset `O`, by the exact profile).
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ObservationSelector {
83    /// Raw output byte range `[offset, offset + len)`.
84    ByteRange {
85        /// First output byte.
86        offset: u64,
87        /// Number of bytes; must be non-empty.
88        len: u64,
89    },
90    /// The full physical byte extent of indirect object `object generation`
91    /// (the `N G obj` introducer through `endobj`).
92    PdfIndirectObject {
93        /// PDF object number.
94        object: u32,
95        /// PDF generation number.
96        generation: u16,
97    },
98    /// The physical encoded stream data span of the stream in indirect object
99    /// `object generation`: the compressed bytes exactly as they appear in the
100    /// source.
101    PdfEncodedStream {
102        /// PDF object number of the enclosing indirect object.
103        object: u32,
104        /// PDF generation number of the enclosing indirect object.
105        generation: u16,
106    },
107    /// Every source byte belonging to incremental-update revision `index`.
108    PdfRevision {
109        /// Zero-based revision index.
110        index: u32,
111    },
112}
113
114/// Cost attribution for one observation.
115///
116/// All fields are measured from the evaluated path, not estimated.
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub struct ObservationStats {
119    /// Number of ops the observation actually evaluated.
120    pub ops_evaluated: usize,
121    /// Total ops in the descriptor's reconstruction program.
122    pub ops_total: usize,
123    /// Number of distinct raw objects read by evaluated ops.
124    pub objects_fetched: usize,
125    /// Total raw objects in the descriptor.
126    pub objects_total: usize,
127    /// Number of entropy channels decoded for this observation.
128    pub channels_decoded: usize,
129    /// Total entropy channels in the descriptor.
130    pub channels_total: usize,
131    /// Sum of the encoded renormalization payload bytes of the decoded channels.
132    ///
133    /// This is the descriptor-side entropy bytes consumed, not the decoded symbol
134    /// count (which is the referenced channels' `decoded_length`). It is a
135    /// **subset** of [`Self::descriptor_bytes_traversed`], which already counts
136    /// the same referenced channel payloads, so it is a breakdown and must
137    /// **never** be added to that field — the sum double-counts.
138    pub entropy_bytes_decoded: u64,
139    /// Approximate descriptor bytes the path needed (see the module docs).
140    ///
141    /// The honest decode-side bound: the graph record, the index record, the
142    /// referenced object payloads, and the referenced channel payloads (which
143    /// include [`Self::entropy_bytes_decoded`]).
144    pub descriptor_bytes_traversed: u64,
145    /// Number of output bytes served.
146    pub output_bytes: u64,
147    /// Real bytes read from the source by the seek reader.
148    ///
149    /// Zero for the in-memory Phase-7 path, which is handed a fully parsed
150    /// descriptor and performs no I/O of its own. This is a *real* I/O figure and
151    /// is distinct from [`Self::descriptor_bytes_traversed`], which is a CPU-side
152    /// approximation kept for comparison.
153    pub bytes_read: u64,
154    /// Whether the whole-source archival digest was verified for this report.
155    ///
156    /// Always `false` for an observation view: a partial read cannot recompute the
157    /// whole-source SHA-256, so a served slice is an *observation* consistent with
158    /// the descriptor's own validated program/index/directory -- not a verified
159    /// archival read. Only `materialize`/`decode`/`verify` check `INTEGRITY` and
160    /// are the archival authority.
161    pub integrity_verified: bool,
162}
163
164impl ObservationStats {
165    /// Fraction of the program evaluated: `ops_evaluated / ops_total`.
166    ///
167    /// Returns `0.0` when the program has no ops, so the ratio is always finite.
168    pub fn work_amplification(&self) -> f64 {
169        if self.ops_total == 0 {
170            0.0
171        } else {
172            self.ops_evaluated as f64 / self.ops_total as f64
173        }
174    }
175}
176
177/// A served observation: the exact requested bytes plus its cost attribution.
178#[derive(Debug, Clone, PartialEq)]
179pub struct ObservationReport {
180    /// The resolved half-open output range `[a, b)` that was served. Reporting it
181    /// lets a measurement harness compare the slice against the source and price
182    /// the sequential baselines at the same output offset.
183    pub range: (u64, u64),
184    /// The exact requested output range.
185    pub bytes: Vec<u8>,
186    /// Measured cost attribution for serving that range.
187    pub stats: ObservationStats,
188}
189
190/// The op window an observation range selects, shared by the in-memory
191/// (Phase-7) and seek (Phase-8) readers so the two selection paths cannot
192/// diverge.
193#[derive(Debug, Clone)]
194pub(crate) struct OpWindow {
195    /// The ops to evaluate, in program order.
196    pub ops: Vec<Op>,
197    /// The absolute output offset the evaluated buffer begins at. Zero when the
198    /// program was walked as a prefix (a position-dependent op forced a fallback).
199    pub buf_start: u64,
200    /// Total ops in the descriptor's program (for `work_amplification`).
201    pub ops_total: usize,
202}
203
204/// Select the minimal op set serving output range `[a, b)`.
205///
206/// This is the exact Phase-7 selection, factored out so `materialize_observation`
207/// and the seek reader share one implementation. For a program of linear,
208/// independent block producers only the ops whose output intersects `[a, b)` are
209/// selected (and `buf_start` is that window's absolute start); any
210/// position-dependent op forces the honest bounded prefix `ops[..=last]` with
211/// `buf_start == 0`.
212pub(crate) fn select_ops(
213    program: &Program,
214    object_lens: &[u64],
215    channel_lens: &[u64],
216    a: u64,
217    b: u64,
218    limits: Limits,
219) -> Result<OpWindow> {
220    let per_op = program.analyze_ops(object_lens, channel_lens, limits)?;
221    select_ops_from_lengths(program, &per_op, a, b)
222}
223
224/// Select the minimal op set serving output range `[a, b)` from an explicit
225/// per-op output-length vector (program order).
226///
227/// This is the length-agnostic core of [`select_ops`], factored so a partial
228/// reader that learns per-op lengths from a validated observation-index op table
229/// can select a window with **exactly** the same linear-skipping vs. bounded-
230/// prefix rule as the in-memory path. The caller is responsible for the lengths
231/// being the authoritative ones (the program's own `analyze_ops` view).
232pub(crate) fn select_ops_from_lengths(
233    program: &Program,
234    per_op: &[u64],
235    a: u64,
236    b: u64,
237) -> Result<OpWindow> {
238    let mut starts: Vec<u64> = Vec::with_capacity(per_op.len());
239    let mut ends: Vec<u64> = Vec::with_capacity(per_op.len());
240    let mut acc: u64 = 0;
241    for &len in per_op {
242        starts.push(acc);
243        acc = acc
244            .checked_add(len)
245            .ok_or_else(|| Error::invalid_graph("observation op length overflow"))?;
246        ends.push(acc);
247    }
248
249    let intersects = |i: usize| per_op[i] > 0 && starts[i] < b && ends[i] > a;
250    let first = (0..per_op.len())
251        .find(|&i| intersects(i))
252        .ok_or_else(|| Error::invalid_graph("observation range is not covered by the program"))?;
253    let last = (0..per_op.len())
254        .rev()
255        .find(|&i| intersects(i))
256        .ok_or_else(|| Error::invalid_graph("observation range is not covered by the program"))?;
257
258    let (ops, buf_start) = if is_linear_independent(program) {
259        let ops = program
260            .ops
261            .iter()
262            .enumerate()
263            .filter(|&(i, _)| intersects(i))
264            .map(|(_, op)| op.clone())
265            .collect();
266        (ops, starts[first])
267    } else {
268        (program.ops[..=last].to_vec(), 0)
269    };
270
271    Ok(OpWindow {
272        ops,
273        buf_start,
274        ops_total: program.ops.len(),
275    })
276}
277
278/// Mark the objects and channels a selected op set references.
279///
280/// The seek reader uses this to decide which `OBJECT`/`ENTROPY_CHANNEL`/`MODEL`
281/// records it must read; the in-memory path uses it to decode lazily. Both paths
282/// must agree, so it lives here.
283pub(crate) fn selection_references(
284    ops: &[Op],
285    objects_len: usize,
286    channels_len: usize,
287) -> (Vec<bool>, Vec<bool>) {
288    let mut objects_used = vec![false; objects_len];
289    let mut channels_used = vec![false; channels_len];
290    for op in ops {
291        mark_references(op, &mut objects_used, &mut channels_used);
292    }
293    (objects_used, channels_used)
294}
295
296/// The served bytes plus the measured cost attribution of one evaluated
297/// selection, before the path-specific `descriptor_bytes_traversed`/`bytes_read`
298/// fields are attached.
299pub(crate) struct ServedSelection {
300    pub bytes: Vec<u8>,
301    pub ops_evaluated: usize,
302    pub ops_total: usize,
303    pub objects_fetched: usize,
304    pub referenced_object_bytes: u64,
305    pub channels_decoded: usize,
306    pub entropy_bytes_decoded: u64,
307    pub referenced_channel_bytes: u64,
308}
309
310/// Evaluate `window` and slice the requested `[a, b)`, decoding only the
311/// referenced channels.
312///
313/// This is the unchanged Phase-7 evaluation body: lazy channel decoding, the
314/// sub-program `eval`, and the window slice. It is shared by both readers; the
315/// seek reader passes partial `objects`/`channels`/`models` vectors where only the
316/// referenced index positions are populated, and every unreferenced position is a
317/// never-dereferenced placeholder.
318#[allow(clippy::too_many_arguments)]
319pub(crate) fn serve_selection(
320    objects: &[Vec<u8>],
321    channels_desc: &[EntropyChannelDescriptor],
322    models: &[EntropyModel],
323    window: OpWindow,
324    objects_used: &[bool],
325    channels_used: &[bool],
326    a: u64,
327    b: u64,
328    limits: Limits,
329) -> Result<ServedSelection> {
330    let channels = decode_referenced_channels(channels_desc, models, channels_used, limits)?;
331
332    let ops_evaluated = window.ops.len();
333    let ops_total = window.ops_total;
334    let buf_start = window.buf_start;
335    let sub = Program::new(window.ops);
336    let out = sub.eval(objects, &channels, limits)?;
337
338    let lo = a
339        .checked_sub(buf_start)
340        .and_then(|v| usize::try_from(v).ok())
341        .ok_or_else(|| Error::internal_invariant("observation window precedes evaluated buffer"))?;
342    let hi = b
343        .checked_sub(buf_start)
344        .and_then(|v| usize::try_from(v).ok())
345        .ok_or_else(|| Error::internal_invariant("observation window overflow"))?;
346    if hi > out.len() {
347        return Err(Error::internal_invariant(
348            "evaluated buffer is shorter than the requested observation window",
349        ));
350    }
351    let bytes = out[lo..hi].to_vec();
352
353    let mut entropy_bytes_decoded: u64 = 0;
354    let mut referenced_channel_bytes: u64 = 0;
355    let mut channels_decoded: usize = 0;
356    for (id, used) in channels_used.iter().enumerate() {
357        if *used {
358            channels_decoded += 1;
359            let payload_len = channels_desc[id].payload.len() as u64;
360            entropy_bytes_decoded += payload_len;
361            referenced_channel_bytes += payload_len;
362        }
363    }
364    let mut objects_fetched: usize = 0;
365    let mut referenced_object_bytes: u64 = 0;
366    for (id, used) in objects_used.iter().enumerate() {
367        if *used {
368            objects_fetched += 1;
369            referenced_object_bytes += objects[id].len() as u64;
370        }
371    }
372
373    Ok(ServedSelection {
374        bytes,
375        ops_evaluated,
376        ops_total,
377        objects_fetched,
378        referenced_object_bytes,
379        channels_decoded,
380        entropy_bytes_decoded,
381        referenced_channel_bytes,
382    })
383}
384
385/// Serve one observation from a parsed descriptor carrying an observation index.
386///
387/// The returned bytes equal `materialize(parsed)[a..b]` for the resolved range;
388/// a descriptor without an index is declined (never silently fully
389/// materialized). See the module documentation for the op-selection and lazy
390/// decoding rules, and for the `descriptor_bytes_traversed` caveat.
391pub fn materialize_observation(
392    parsed: &ParsedDescriptor,
393    selector: ObservationSelector,
394    limits: Limits,
395) -> Result<ObservationReport> {
396    let d = &parsed.descriptor;
397    let index = d
398        .observation_index
399        .as_ref()
400        .ok_or_else(|| Error::unsupported_feature("descriptor has no observation index"))?;
401
402    // The in-memory observation lane takes no resolver; a descriptor carrying
403    // external object references is declined rather than partially served.
404    if d.objects
405        .iter()
406        .any(|o| matches!(o, crate::container::ObjectSource::External { .. }))
407    {
408        return Err(Error::unsupported_feature(
409            "partial observation cannot resolve external objects (no resolver supplied)",
410        ));
411    }
412    let objects: Vec<Vec<u8>> = d
413        .objects
414        .iter()
415        .map(|o| o.as_inline().unwrap_or(&[]).to_vec())
416        .collect();
417
418    // 2. Resolve the selector to an output range `[a, b)`.
419    let (a, b) = resolve_selector(index, selector, d.source_len)?;
420
421    // 3. Per-op lengths and cumulative offsets. The program is authoritative; the
422    //    index's op table was already cross-checked against it at parse time.
423    let object_lens: Vec<u64> = objects.iter().map(|o| o.len() as u64).collect();
424    let channel_lens: Vec<u64> = d.channels.iter().map(|c| c.decoded_length).collect();
425
426    // 4. Selection and lazy evaluation, shared verbatim with the seek reader.
427    let window = select_ops(&d.program, &object_lens, &channel_lens, a, b, limits)?;
428    let (objects_used, channels_used) =
429        selection_references(&window.ops, objects.len(), d.channels.len());
430    let served = serve_selection(
431        &objects,
432        &d.channels,
433        &d.models,
434        window,
435        &objects_used,
436        &channels_used,
437        a,
438        b,
439        limits,
440    )?;
441
442    let descriptor_bytes_traversed = parsed.cost.graph
443        + parsed.cost.index
444        + served.referenced_object_bytes
445        + served.referenced_channel_bytes;
446
447    let stats = ObservationStats {
448        ops_evaluated: served.ops_evaluated,
449        ops_total: served.ops_total,
450        objects_fetched: served.objects_fetched,
451        objects_total: objects.len(),
452        channels_decoded: served.channels_decoded,
453        channels_total: d.channels.len(),
454        entropy_bytes_decoded: served.entropy_bytes_decoded,
455        descriptor_bytes_traversed,
456        output_bytes: served.bytes.len() as u64,
457        bytes_read: 0,
458        integrity_verified: false,
459    };
460
461    Ok(ObservationReport {
462        range: (a, b),
463        bytes: served.bytes,
464        stats,
465    })
466}
467
468/// Resolve a selector to a target output range `[a, b)`.
469pub(crate) fn resolve_selector(
470    index: &ObservationIndex,
471    selector: ObservationSelector,
472    source_len: u64,
473) -> Result<(u64, u64)> {
474    match selector {
475        ObservationSelector::ByteRange { offset, len } => {
476            if len == 0 {
477                return Err(Error::usage("observation byte range must be non-empty"));
478            }
479            let end = offset
480                .checked_add(len)
481                .ok_or_else(|| Error::usage("observation byte range overflows"))?;
482            if end > source_len {
483                return Err(Error::usage(format!(
484                    "observation byte range {offset}..{end} exceeds source length {source_len}"
485                )));
486            }
487            Ok((offset, end))
488        }
489        ObservationSelector::PdfIndirectObject { object, generation } => resolve_pdf(
490            index,
491            SELECTOR_OBJECT,
492            object,
493            u32::from(generation),
494            "indirect object",
495        ),
496        ObservationSelector::PdfEncodedStream { object, generation } => resolve_pdf(
497            index,
498            SELECTOR_STREAM,
499            object,
500            u32::from(generation),
501            "encoded stream",
502        ),
503        ObservationSelector::PdfRevision { index: rev } => {
504            if index.section_flags & SECTION_PDF_SELECTORS == 0 {
505                return Err(Error::unsupported_feature(
506                    "observation index has no PDF selector table",
507                ));
508            }
509            let hits: Vec<&IndexSelector> = index
510                .selectors
511                .iter()
512                .filter(|s| s.kind == SELECTOR_REVISION && s.number == rev)
513                .collect();
514            match hits.as_slice() {
515                [one] => selector_range(one, "PDF revision", u64::from(rev)),
516                [] => Err(Error::unsupported_feature(format!(
517                    "observation index has no selector for PDF revision {rev}"
518                ))),
519                _ => Err(Error::invalid_container(format!(
520                    "observation index has ambiguous selectors for PDF revision {rev}"
521                ))),
522            }
523        }
524    }
525}
526
527/// Look up a `(kind, number, generation)` PDF selector; absent or ambiguous is a
528/// typed error (never a guess).
529fn resolve_pdf(
530    index: &ObservationIndex,
531    kind: u8,
532    number: u32,
533    generation: u32,
534    label: &str,
535) -> Result<(u64, u64)> {
536    if index.section_flags & SECTION_PDF_SELECTORS == 0 {
537        return Err(Error::unsupported_feature(
538            "observation index has no PDF selector table",
539        ));
540    }
541    let hits: Vec<&IndexSelector> = index
542        .selectors
543        .iter()
544        .filter(|s| s.kind == kind && s.number == number && s.generation == generation)
545        .collect();
546    match hits.as_slice() {
547        [one] => selector_range(one, label, u64::from(number)),
548        [] => Err(Error::unsupported_feature(format!(
549            "observation index has no selector for {label} {number} {generation}"
550        ))),
551        _ => Err(Error::invalid_container(format!(
552            "observation index has ambiguous selectors for {label} {number} {generation}"
553        ))),
554    }
555}
556
557/// Convert a validated index selector into a checked `[a, b)` range.
558fn selector_range(selector: &IndexSelector, label: &str, id: u64) -> Result<(u64, u64)> {
559    let end = selector
560        .out_off
561        .checked_add(selector.out_len)
562        .ok_or_else(|| {
563            Error::invalid_container(format!("{label} {id} selector range overflows"))
564        })?;
565    Ok((selector.out_off, end))
566}
567
568/// True when every op is a linear, independently evaluable block producer.
569///
570/// Position-dependent ops (`REPEAT_LAST`, `MARK_OFFSET`, `EMIT_OFFSET`,
571/// `PACK_SEGMENTS`, `PACKED_CHANNELS`) disqualify skipping because their output
572/// depends on the running output position or on a previous block's bytes.
573fn is_linear_independent(program: &Program) -> bool {
574    program.ops.iter().all(|op| {
575        matches!(
576            op,
577            Op::EmitObject { .. }
578                | Op::Inline { .. }
579                | Op::DecodeChannel { .. }
580                | Op::DeflateReplay { .. }
581                | Op::InterleaveChannels { .. }
582        )
583    })
584}
585
586/// Record the objects and channels an op reads from.
587fn mark_references(op: &Op, objects: &mut [bool], channels: &mut [bool]) {
588    match op {
589        Op::EmitObject { object_id } => mark(objects, *object_id),
590        Op::Inline { .. }
591        | Op::MarkOffset { .. }
592        | Op::EmitOffset { .. }
593        | Op::RepeatLast { .. } => {}
594        Op::DecodeChannel { channel_id } => mark(channels, *channel_id),
595        Op::InterleaveChannels {
596            kinds_channel,
597            lengths_channel,
598            first_payload_channel,
599            payload_channel_count,
600        } => {
601            mark(channels, *kinds_channel);
602            mark(channels, *lengths_channel);
603            for k in 0..u32::from(*payload_channel_count) {
604                if let Some(id) = first_payload_channel.checked_add(k) {
605                    mark(channels, id);
606                }
607            }
608        }
609        Op::PackSegments { data_object, .. } => mark(objects, *data_object),
610        Op::PackedChannels {
611            data_channel,
612            plan_channel,
613            ..
614        } => {
615            mark(channels, *data_channel);
616            mark(channels, *plan_channel);
617        }
618        Op::DeflateReplay {
619            source_kind,
620            source_id,
621            corrections_object,
622            ..
623        } => {
624            match *source_kind {
625                crate::dra::op::DEFLATE_SOURCE_OBJECT => mark(objects, *source_id),
626                crate::dra::op::DEFLATE_SOURCE_CHANNEL => mark(channels, *source_id),
627                _ => {}
628            }
629            mark(objects, *corrections_object);
630        }
631    }
632}
633
634/// Mark `id` in `flags` when it is in range; an out-of-range id is left to the
635/// evaluator's own validation.
636fn mark(flags: &mut [bool], id: u32) {
637    if let Some(slot) = flags.get_mut(id as usize) {
638        *slot = true;
639    }
640}
641
642/// Decode only the entropy channels referenced by the evaluated ops.
643#[cfg(feature = "rans")]
644fn decode_referenced_channels(
645    channels_desc: &[EntropyChannelDescriptor],
646    models: &[EntropyModel],
647    channels_used: &[bool],
648    limits: Limits,
649) -> Result<Vec<Vec<u8>>> {
650    let mut channels: Vec<Vec<u8>> = vec![Vec::new(); channels_desc.len()];
651    for (id, used) in channels_used.iter().enumerate() {
652        if *used {
653            channels[id] = decode_channel_by_id(channels_desc, models, id, limits)?;
654        }
655    }
656    Ok(channels)
657}
658
659/// Without the `rans` feature a referenced channel cannot be decoded; decline
660/// explicitly rather than silently producing wrong bytes.
661#[cfg(not(feature = "rans"))]
662fn decode_referenced_channels(
663    channels_desc: &[EntropyChannelDescriptor],
664    _models: &[EntropyModel],
665    channels_used: &[bool],
666    _limits: Limits,
667) -> Result<Vec<Vec<u8>>> {
668    if channels_used.iter().any(|&used| used) {
669        return Err(Error::unsupported_feature(
670            "this build was compiled without the `rans` feature",
671        ));
672    }
673    Ok(vec![Vec::new(); channels_desc.len()])
674}
675
676/// Decode one referenced channel against the model it names.
677#[cfg(feature = "rans")]
678fn decode_channel_by_id(
679    channels_desc: &[EntropyChannelDescriptor],
680    models: &[EntropyModel],
681    id: usize,
682    limits: Limits,
683) -> Result<Vec<u8>> {
684    let channel = channels_desc.get(id).ok_or_else(|| {
685        Error::invalid_model(format!(
686            "observation references missing entropy channel {id}"
687        ))
688    })?;
689    let model = models.get(channel.model_id as usize).ok_or_else(|| {
690        Error::invalid_model(format!(
691            "entropy channel {id} references missing model {}",
692            channel.model_id
693        ))
694    })?;
695    let capsule = Capsule {
696        initial_state: channel.initial_state,
697        payload: channel.payload.clone(),
698        symbol_count: channel.symbol_count,
699        decoded_length: channel.decoded_length,
700    };
701    decode_channel(model, &capsule, limits)
702}