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}