1use 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#[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 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 pub fn bytes_read(&self) -> u64 {
89 self.bytes_read
90 }
91
92 pub fn read_calls(&self) -> u32 {
94 self.read_calls
95 }
96
97 pub fn seeks(&self) -> u32 {
99 self.seeks
100 }
101
102 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
124type Selection = (u64, u64, OpWindow, Vec<bool>, Vec<bool>);
127
128pub 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 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 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 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 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 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 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 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 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
382fn 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
394fn 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
410fn 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
437fn 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
454fn placeholder_model() -> EntropyModel {
456 EntropyModel {
457 scale_bits: 0,
458 frequencies: Vec::new(),
459 }
460}