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 { .. } => resolve_byte_range(selector, source_len),
476 ObservationSelector::PdfIndirectObject { object, generation } => resolve_pdf(
477 index,
478 SELECTOR_OBJECT,
479 object,
480 u32::from(generation),
481 "indirect object",
482 ),
483 ObservationSelector::PdfEncodedStream { object, generation } => resolve_pdf(
484 index,
485 SELECTOR_STREAM,
486 object,
487 u32::from(generation),
488 "encoded stream",
489 ),
490 ObservationSelector::PdfRevision { index: rev } => {
491 if index.section_flags & SECTION_PDF_SELECTORS == 0 {
492 return Err(Error::unsupported_feature(
493 "observation index has no PDF selector table",
494 ));
495 }
496 let hits: Vec<&IndexSelector> = index
497 .selectors
498 .iter()
499 .filter(|s| s.kind == SELECTOR_REVISION && s.number == rev)
500 .collect();
501 match hits.as_slice() {
502 [one] => selector_range(one, "PDF revision", u64::from(rev)),
503 [] => Err(Error::unsupported_feature(format!(
504 "observation index has no selector for PDF revision {rev}"
505 ))),
506 _ => Err(Error::invalid_container(format!(
507 "observation index has ambiguous selectors for PDF revision {rev}"
508 ))),
509 }
510 }
511 }
512}
513
514/// Resolve a raw byte-range selector to `[offset, offset+len)`.
515///
516/// This is the one selector that needs no `OBSERVATION_INDEX`; the checkpoint
517/// lane of the seek reader uses it directly. It is shared with
518/// [`resolve_selector`] so the two cannot diverge.
519pub(crate) fn resolve_byte_range(
520 selector: ObservationSelector,
521 source_len: u64,
522) -> Result<(u64, u64)> {
523 match selector {
524 ObservationSelector::ByteRange { offset, len } => {
525 if len == 0 {
526 return Err(Error::usage("observation byte range must be non-empty"));
527 }
528 let end = offset
529 .checked_add(len)
530 .ok_or_else(|| Error::usage("observation byte range overflows"))?;
531 if end > source_len {
532 return Err(Error::usage(format!(
533 "observation byte range {offset}..{end} exceeds source length {source_len}"
534 )));
535 }
536 Ok((offset, end))
537 }
538 _ => Err(Error::internal_invariant(
539 "resolve_byte_range called with a non-byte-range selector",
540 )),
541 }
542}
543
544/// Look up a `(kind, number, generation)` PDF selector; absent or ambiguous is a
545/// typed error (never a guess).
546fn resolve_pdf(
547 index: &ObservationIndex,
548 kind: u8,
549 number: u32,
550 generation: u32,
551 label: &str,
552) -> Result<(u64, u64)> {
553 if index.section_flags & SECTION_PDF_SELECTORS == 0 {
554 return Err(Error::unsupported_feature(
555 "observation index has no PDF selector table",
556 ));
557 }
558 let hits: Vec<&IndexSelector> = index
559 .selectors
560 .iter()
561 .filter(|s| s.kind == kind && s.number == number && s.generation == generation)
562 .collect();
563 match hits.as_slice() {
564 [one] => selector_range(one, label, u64::from(number)),
565 [] => Err(Error::unsupported_feature(format!(
566 "observation index has no selector for {label} {number} {generation}"
567 ))),
568 _ => Err(Error::invalid_container(format!(
569 "observation index has ambiguous selectors for {label} {number} {generation}"
570 ))),
571 }
572}
573
574/// Convert a validated index selector into a checked `[a, b)` range.
575fn selector_range(selector: &IndexSelector, label: &str, id: u64) -> Result<(u64, u64)> {
576 let end = selector
577 .out_off
578 .checked_add(selector.out_len)
579 .ok_or_else(|| {
580 Error::invalid_container(format!("{label} {id} selector range overflows"))
581 })?;
582 Ok((selector.out_off, end))
583}
584
585/// True when every op is a linear, independently evaluable block producer.
586///
587/// Position-dependent ops (`REPEAT_LAST`, `MARK_OFFSET`, `EMIT_OFFSET`,
588/// `PACK_SEGMENTS`, `PACKED_CHANNELS`) disqualify skipping because their output
589/// depends on the running output position or on a previous block's bytes.
590fn is_linear_independent(program: &Program) -> bool {
591 program.ops.iter().all(|op| {
592 matches!(
593 op,
594 Op::EmitObject { .. }
595 | Op::Inline { .. }
596 | Op::DecodeChannel { .. }
597 | Op::DeflateReplay { .. }
598 | Op::InterleaveChannels { .. }
599 )
600 })
601}
602
603/// Record the objects and channels an op reads from.
604fn mark_references(op: &Op, objects: &mut [bool], channels: &mut [bool]) {
605 match op {
606 Op::EmitObject { object_id } => mark(objects, *object_id),
607 Op::Inline { .. }
608 | Op::MarkOffset { .. }
609 | Op::EmitOffset { .. }
610 | Op::RepeatLast { .. } => {}
611 Op::DecodeChannel { channel_id } => mark(channels, *channel_id),
612 Op::InterleaveChannels {
613 kinds_channel,
614 lengths_channel,
615 first_payload_channel,
616 payload_channel_count,
617 } => {
618 mark(channels, *kinds_channel);
619 mark(channels, *lengths_channel);
620 for k in 0..u32::from(*payload_channel_count) {
621 if let Some(id) = first_payload_channel.checked_add(k) {
622 mark(channels, id);
623 }
624 }
625 }
626 Op::PackSegments { data_object, .. } => mark(objects, *data_object),
627 Op::PackedChannels {
628 data_channel,
629 plan_channel,
630 ..
631 } => {
632 mark(channels, *data_channel);
633 mark(channels, *plan_channel);
634 }
635 Op::DeflateReplay {
636 source_kind,
637 source_id,
638 corrections_object,
639 ..
640 } => {
641 match *source_kind {
642 crate::dra::op::DEFLATE_SOURCE_OBJECT => mark(objects, *source_id),
643 crate::dra::op::DEFLATE_SOURCE_CHANNEL => mark(channels, *source_id),
644 _ => {}
645 }
646 mark(objects, *corrections_object);
647 }
648 }
649}
650
651/// Mark `id` in `flags` when it is in range; an out-of-range id is left to the
652/// evaluator's own validation.
653fn mark(flags: &mut [bool], id: u32) {
654 if let Some(slot) = flags.get_mut(id as usize) {
655 *slot = true;
656 }
657}
658
659/// Decode only the entropy channels referenced by the evaluated ops.
660#[cfg(feature = "rans")]
661fn decode_referenced_channels(
662 channels_desc: &[EntropyChannelDescriptor],
663 models: &[EntropyModel],
664 channels_used: &[bool],
665 limits: Limits,
666) -> Result<Vec<Vec<u8>>> {
667 let mut channels: Vec<Vec<u8>> = vec![Vec::new(); channels_desc.len()];
668 for (id, used) in channels_used.iter().enumerate() {
669 if *used {
670 channels[id] = decode_channel_by_id(channels_desc, models, id, limits)?;
671 }
672 }
673 Ok(channels)
674}
675
676/// Without the `rans` feature a referenced channel cannot be decoded; decline
677/// explicitly rather than silently producing wrong bytes.
678#[cfg(not(feature = "rans"))]
679fn decode_referenced_channels(
680 channels_desc: &[EntropyChannelDescriptor],
681 _models: &[EntropyModel],
682 channels_used: &[bool],
683 _limits: Limits,
684) -> Result<Vec<Vec<u8>>> {
685 if channels_used.iter().any(|&used| used) {
686 return Err(Error::unsupported_feature(
687 "this build was compiled without the `rans` feature",
688 ));
689 }
690 Ok(vec![Vec::new(); channels_desc.len()])
691}
692
693/// Decode one referenced channel against the model it names.
694#[cfg(feature = "rans")]
695fn decode_channel_by_id(
696 channels_desc: &[EntropyChannelDescriptor],
697 models: &[EntropyModel],
698 id: usize,
699 limits: Limits,
700) -> Result<Vec<u8>> {
701 let channel = channels_desc.get(id).ok_or_else(|| {
702 Error::invalid_model(format!(
703 "observation references missing entropy channel {id}"
704 ))
705 })?;
706 let model = models.get(channel.model_id as usize).ok_or_else(|| {
707 Error::invalid_model(format!(
708 "entropy channel {id} references missing model {}",
709 channel.model_id
710 ))
711 })?;
712 let capsule = Capsule {
713 initial_state: channel.initial_state,
714 payload: channel.payload.clone(),
715 symbol_count: channel.symbol_count,
716 decoded_length: channel.decoded_length,
717 };
718 decode_channel(model, &capsule, limits)
719}