1use crate::EXACTNESS_PROFILE_EXACT_BYTES;
9use crate::accounting::CostBreakdown;
10use crate::container::checkpoint::CheckpointTable;
11use crate::container::directory::{
12 DirectoryEntry, RecordSite, SEEK_DIRECTORY_ALL_SECTIONS, SeekDirectory,
13};
14use crate::container::header::{HEADER_LEN, Header, MAGIC};
15use crate::container::observation::ObservationIndex;
16use crate::container::record::{FLAG_OPTIONAL, RECORD_OVERHEAD, RecordReader, RecordTag};
17use crate::dra::Program;
18use crate::entropy::codec::EntropyChannelDescriptor;
19use crate::entropy::model::EntropyModel;
20use crate::error::{Error, Result};
21use crate::integrity::sha256;
22use crate::limits::Limits;
23use crate::store::Id;
24
25pub const EXTERNAL_REF_PAYLOAD_LEN: usize = 40;
27
28pub const UNIVERSE: &str = "vole-document;universe;phase9;exact-bytes;dra-8;opaque+entropy+pdf+channels+offsets+packed+packed-channels+deflate-replay-preflate-0.7.6-experimental+observation-index-v1+seek-directory-v1+external-objects-v1";
42
43pub fn universe_id_from_str(universe: &str) -> [u8; 16] {
45 let full = sha256(universe.as_bytes());
46 let mut id = [0u8; 16];
47 id.copy_from_slice(&full[0..16]);
48 id
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ObjectSource {
58 Inline(Vec<u8>),
60 External { id: Id, len: u64 },
63}
64
65impl ObjectSource {
66 pub fn len(&self) -> u64 {
68 match self {
69 ObjectSource::Inline(b) => b.len() as u64,
70 ObjectSource::External { len, .. } => *len,
71 }
72 }
73
74 pub fn is_empty(&self) -> bool {
76 self.len() == 0
77 }
78
79 pub fn as_inline(&self) -> Option<&[u8]> {
81 match self {
82 ObjectSource::Inline(b) => Some(b),
83 ObjectSource::External { .. } => None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct Descriptor {
91 pub universe: String,
93 pub source_format: u8,
95 pub format_basis: String,
97 pub models: Vec<EntropyModel>,
99 pub channels: Vec<EntropyChannelDescriptor>,
101 pub objects: Vec<ObjectSource>,
103 pub program: Program,
105 pub observation_index: Option<ObservationIndex>,
111 pub seek_directory: bool,
118 pub checkpoints: Option<CheckpointTable>,
127 pub source_sha256: [u8; 32],
129 pub source_len: u64,
131}
132
133#[derive(Debug, Clone)]
135pub struct ParsedDescriptor {
136 pub descriptor: Descriptor,
138 pub cost: CostBreakdown,
140 pub universe_id: [u8; 16],
142}
143
144struct PendingRecord {
146 tag: RecordTag,
147 flags: u8,
148 payload: Vec<u8>,
149}
150
151impl PendingRecord {
152 fn new(tag: RecordTag, flags: u8, payload: Vec<u8>) -> Self {
153 PendingRecord {
154 tag,
155 flags,
156 payload,
157 }
158 }
159}
160
161impl Descriptor {
162 pub fn header(&self) -> Header {
164 let mut header = Header::new(
165 universe_id_from_str(&self.universe),
166 self.source_len,
167 EXACTNESS_PROFILE_EXACT_BYTES,
168 self.source_format,
169 );
170 header.mandatory_features = self.required_features();
171 header.optional_features = self.optional_features();
172 header
173 }
174
175 pub fn required_features(&self) -> u32 {
182 let mut bits = 0u32;
183 for op in &self.program.ops {
184 if matches!(op, crate::dra::Op::DeflateReplay { .. }) {
185 bits |= crate::container::header::FEATURE_DEFLATE_REPLAY;
186 }
187 }
188 if self
189 .objects
190 .iter()
191 .any(|o| matches!(o, ObjectSource::External { .. }))
192 {
193 bits |= crate::container::header::FEATURE_EXTERNAL_OBJECTS;
194 }
195 bits
196 }
197
198 pub fn optional_features(&self) -> u32 {
205 let mut bits = 0u32;
206 if self.observation_index.is_some() {
207 bits |= crate::container::header::FEATURE_OBSERVATION_INDEX;
208 }
209 if self.seek_directory {
210 bits |= crate::container::header::FEATURE_SEEK_DIRECTORY;
211 }
212 if self.checkpoints.is_some() {
213 bits |= crate::container::header::FEATURE_CHECKPOINTS;
214 }
215 bits
216 }
217
218 pub fn serialize(&self) -> Result<(Vec<u8>, CostBreakdown)> {
227 if self.seek_directory && self.observation_index.is_none() {
228 return Err(Error::invalid_container(
229 "a seek directory requires an observation index",
230 ));
231 }
232 if self.checkpoints.is_some() && !self.seek_directory {
233 return Err(Error::invalid_container(
234 "a checkpoint requires a seek directory to locate it",
235 ));
236 }
237
238 let mut cost = CostBreakdown {
239 header: HEADER_LEN as u64,
240 ..Default::default()
241 };
242
243 let mut pending: Vec<PendingRecord> = Vec::new();
245
246 pending.push(PendingRecord::new(
248 RecordTag::Universe,
249 0,
250 self.universe.as_bytes().to_vec(),
251 ));
252 cost.universe = self.universe.len() as u64;
253
254 let basis = self.format_basis.as_bytes();
256 let basis_len = u32::try_from(basis.len())
257 .map_err(|_| Error::resource_limit("format basis too long"))?;
258 let mut fmt = Vec::with_capacity(5 + basis.len());
259 fmt.push(self.source_format);
260 fmt.extend_from_slice(&basis_len.to_le_bytes());
261 fmt.extend_from_slice(basis);
262 cost.format = fmt.len() as u64;
263 pending.push(PendingRecord::new(RecordTag::Format, 0, fmt));
264
265 for model in &self.models {
267 let encoded = model.encode()?;
268 cost.models += encoded.len() as u64;
269 pending.push(PendingRecord::new(RecordTag::Model, 0, encoded));
270 }
271
272 for channel in &self.channels {
274 let encoded = channel.encode()?;
275 cost.entropy_payload += encoded.len() as u64;
276 pending.push(PendingRecord::new(RecordTag::EntropyChannel, 0, encoded));
277 }
278
279 for obj in &self.objects {
282 match obj {
283 ObjectSource::Inline(bytes) => {
284 cost.objects += bytes.len() as u64;
285 pending.push(PendingRecord::new(RecordTag::Object, 0, bytes.clone()));
286 }
287 ObjectSource::External { id, len } => {
288 let mut payload = Vec::with_capacity(EXTERNAL_REF_PAYLOAD_LEN);
289 payload.extend_from_slice(id.as_bytes());
290 payload.extend_from_slice(&len.to_le_bytes());
291 cost.external_refs += payload.len() as u64;
292 pending.push(PendingRecord::new(RecordTag::ExternalRef, 0, payload));
293 }
294 }
295 }
296
297 let graph = self.program.encode()?;
299 cost.graph = graph.len() as u64;
300 pending.push(PendingRecord::new(RecordTag::Graph, 0, graph));
301
302 let mut index_records: u64 = 0;
305 if let Some(index) = &self.observation_index {
306 let payload = index.encode()?;
307 index_records = 1;
308 cost.index = payload.len() as u64 + RECORD_OVERHEAD as u64;
309 pending.push(PendingRecord::new(
310 RecordTag::ObservationIndex,
311 FLAG_OPTIONAL,
312 payload,
313 ));
314 }
315
316 let mut checkpoint_records: u64 = 0;
321 if let Some(checkpoint) = &self.checkpoints {
322 let payload = checkpoint.encode()?;
323 checkpoint_records = 1;
324 cost.checkpoints = payload.len() as u64 + RECORD_OVERHEAD as u64;
325 pending.push(PendingRecord::new(
326 RecordTag::Checkpoint,
327 FLAG_OPTIONAL,
328 payload,
329 ));
330 }
331
332 let mut integ = Vec::with_capacity(40);
334 integ.extend_from_slice(&self.source_sha256);
335 integ.extend_from_slice(&self.source_len.to_le_bytes());
336 cost.integrity = integ.len() as u64;
337 pending.push(PendingRecord::new(RecordTag::Integrity, 0, integ));
338
339 let mut directory_payload: Option<Vec<u8>> = None;
342 let mut directory_records: u64 = 0;
343 if self.seek_directory {
344 let channel_lengths: Vec<u64> =
345 self.channels.iter().map(|c| c.decoded_length).collect();
346
347 let mut entries: Vec<DirectoryEntry> = Vec::with_capacity(pending.len() + 2);
351 entries.push(DirectoryEntry {
352 tag: RecordTag::Directory as u8,
353 offset: HEADER_LEN as u64,
354 payload_len: 0,
355 });
356 for rec in &pending {
357 let payload_len = u32::try_from(rec.payload.len())
358 .map_err(|_| Error::resource_limit("record payload exceeds u32"))?;
359 entries.push(DirectoryEntry {
360 tag: rec.tag as u8,
361 offset: 0,
362 payload_len,
363 });
364 }
365 entries.push(DirectoryEntry {
366 tag: RecordTag::Trailer as u8,
367 offset: 0,
368 payload_len: 20,
369 });
370 let classes = crate::container::directory::class_index(&entries);
371
372 let mut dir = SeekDirectory {
376 section_flags: SEEK_DIRECTORY_ALL_SECTIONS,
377 entries,
378 classes,
379 channel_lengths,
380 };
381 let dir_payload_len = dir.encode()?.len();
382 dir.entries[0].payload_len = u32::try_from(dir_payload_len)
383 .map_err(|_| Error::resource_limit("seek directory payload exceeds u32"))?;
384
385 let mut off = (HEADER_LEN + RECORD_OVERHEAD + dir_payload_len) as u64;
386 let last = dir.entries.len() - 1;
387 for entry in &mut dir.entries[1..last] {
388 entry.offset = off;
389 off = off
390 .checked_add(RECORD_OVERHEAD as u64)
391 .and_then(|v| v.checked_add(u64::from(entry.payload_len)))
392 .ok_or_else(|| Error::invalid_container("seek directory offset overflow"))?;
393 }
394 dir.entries[last].offset = off;
395
396 let payload = dir.encode()?;
397 debug_assert_eq!(payload.len(), dir_payload_len);
398 cost.directory = dir_payload_len as u64 + RECORD_OVERHEAD as u64;
399 directory_records = 1;
400 directory_payload = Some(payload);
401 }
402
403 let mut out = Vec::new();
405 out.extend_from_slice(&self.header().encode());
406
407 if let Some(payload) = &directory_payload {
408 crate::container::record::write_record(
409 &mut out,
410 RecordTag::Directory as u8,
411 FLAG_OPTIONAL,
412 payload,
413 )?;
414 }
415 for rec in &pending {
416 crate::container::record::write_record(
417 &mut out,
418 rec.tag as u8,
419 rec.flags,
420 &rec.payload,
421 )?;
422 }
423
424 let total_records = pending.len() as u64 + directory_records + 1;
427 let total_records =
428 u32::try_from(total_records).map_err(|_| Error::resource_limit("too many records"))?;
429 let payload_bytes = (out.len() - HEADER_LEN) as u64;
430 let mut trailer = Vec::with_capacity(20);
431 trailer.extend_from_slice(&total_records.to_le_bytes());
432 trailer.extend_from_slice(&payload_bytes.to_le_bytes());
433 trailer.extend_from_slice(&MAGIC);
434 crate::container::record::write_record(&mut out, RecordTag::Trailer as u8, 0, &trailer)?;
435 cost.trailer = trailer.len() as u64;
436
437 cost.record_framing = RECORD_OVERHEAD as u64
443 * (u64::from(total_records) - index_records - directory_records - checkpoint_records);
444
445 debug_assert_eq!(cost.total(), out.len() as u64);
446 Ok((out, cost))
447 }
448
449 pub fn parse(bytes: &[u8], limits: Limits) -> Result<ParsedDescriptor> {
456 if bytes.len() as u64 > limits.max_input_bytes {
457 return Err(Error::resource_limit(
458 "input exceeds configured input limit",
459 ));
460 }
461 let header = Header::decode(bytes)?;
462 if !header.source_format_supported() {
463 return Err(Error::unsupported_feature(format!(
464 "source format class {} has no adapter in this build",
465 header.source_format
466 )));
467 }
468
469 let mut cost = CostBreakdown {
470 header: HEADER_LEN as u64,
471 ..Default::default()
472 };
473
474 let mut reader = RecordReader::new(bytes, HEADER_LEN, limits);
475 let mut universe: Option<String> = None;
476 let mut format: Option<(u8, String)> = None;
477 let mut models: Vec<EntropyModel> = Vec::new();
478 let mut channels: Vec<EntropyChannelDescriptor> = Vec::new();
479 let mut objects: Vec<ObjectSource> = Vec::new();
480 let mut program: Option<Program> = None;
481 let mut observation_index: Option<ObservationIndex> = None;
482 let mut source_sha256: Option<[u8; 32]> = None;
483 let mut source_len: Option<u64> = None;
484 let mut saw_trailer = false;
485 let mut trailer_record_count: Option<u32> = None;
486 let mut records_seen: u32 = 0;
487 let mut index_records: u64 = 0;
488 let mut directory_records: u64 = 0;
489 let mut seek_directory: Option<SeekDirectory> = None;
490 let mut checkpoint_records: u64 = 0;
491 let mut checkpoints: Option<CheckpointTable> = None;
492 let mut graph_payload: Vec<u8> = Vec::new();
493 let mut sites: Vec<RecordSite> = Vec::new();
494
495 while let Some(rec) = reader.next_record()? {
496 records_seen += 1;
497 let site = RecordSite {
498 tag: rec.tag,
499 offset: reader.position() as u64
500 - (RECORD_OVERHEAD as u64 + rec.payload.len() as u64),
501 payload_len: u32::try_from(rec.payload.len())
502 .map_err(|_| Error::resource_limit("record payload exceeds u32"))?,
503 };
504 sites.push(site);
505 if saw_trailer {
506 return Err(Error::invalid_container("record found after TRAILER"));
507 }
508 match RecordTag::from_u8(rec.tag) {
509 Some(RecordTag::Universe) => {
510 if universe.is_some() {
511 return Err(Error::invalid_container("duplicate UNIVERSE record"));
512 }
513 let payload_len = rec.payload.len();
514 let s = String::from_utf8(rec.payload)
515 .map_err(|_| Error::invalid_container("universe is not valid UTF-8"))?;
516 if universe_id_from_str(&s) != header.universe_id {
517 return Err(Error::invalid_container(
518 "universe declaration does not match its header identifier",
519 ));
520 }
521 universe = Some(s);
522 cost.universe = payload_len as u64;
523 }
524 Some(RecordTag::Format) => {
525 if format.is_some() {
526 return Err(Error::invalid_container("duplicate FORMAT record"));
527 }
528 if rec.payload.len() < 5 {
529 return Err(Error::invalid_container("truncated FORMAT payload"));
530 }
531 let class = rec.payload[0];
532 let blen = u32::from_le_bytes([
533 rec.payload[1],
534 rec.payload[2],
535 rec.payload[3],
536 rec.payload[4],
537 ]);
538 let blen = blen as usize;
539 if rec.payload.len() != 5 + blen {
540 return Err(Error::invalid_container("FORMAT payload length mismatch"));
541 }
542 let basis = String::from_utf8(rec.payload[5..].to_vec())
543 .map_err(|_| Error::invalid_container("format basis is not UTF-8"))?;
544 if class != header.source_format {
545 return Err(Error::invalid_container(
546 "FORMAT class disagrees with header source_format",
547 ));
548 }
549 format = Some((class, basis));
550 cost.format = rec.payload.len() as u64;
551 }
552 Some(RecordTag::Object) => {
553 if objects.len() as u32 >= limits.max_object_count {
554 return Err(Error::resource_limit("object count limit exceeded"));
555 }
556 cost.objects += rec.payload.len() as u64;
557 objects.push(ObjectSource::Inline(rec.payload));
558 }
559 Some(RecordTag::ExternalRef) => {
560 if objects.len() as u32 >= limits.max_object_count {
561 return Err(Error::resource_limit("object count limit exceeded"));
562 }
563 if rec.payload.len() != EXTERNAL_REF_PAYLOAD_LEN {
564 return Err(Error::invalid_container(
565 "EXTERNAL_REF payload must be 40 bytes",
566 ));
567 }
568 let mut id = [0u8; 32];
569 id.copy_from_slice(&rec.payload[0..32]);
570 let len = u64::from_le_bytes([
571 rec.payload[32],
572 rec.payload[33],
573 rec.payload[34],
574 rec.payload[35],
575 rec.payload[36],
576 rec.payload[37],
577 rec.payload[38],
578 rec.payload[39],
579 ]);
580 cost.external_refs += rec.payload.len() as u64;
581 objects.push(ObjectSource::External {
582 id: Id::from_bytes(id),
583 len,
584 });
585 }
586 Some(RecordTag::Model) => {
587 if models.len() as u32 >= limits.max_model_count {
588 return Err(Error::resource_limit("entropy model count limit exceeded"));
589 }
590 if rec.payload.len() as u32 > limits.max_entropy_model_bytes {
591 return Err(Error::resource_limit(format!(
592 "entropy model payload {} exceeds limit {}",
593 rec.payload.len(),
594 limits.max_entropy_model_bytes
595 )));
596 }
597 let model = EntropyModel::decode(&rec.payload)?;
598 cost.models += rec.payload.len() as u64;
599 models.push(model);
600 }
601 Some(RecordTag::EntropyChannel) => {
602 if channels.len() as u32 >= limits.max_channel_count {
603 return Err(Error::resource_limit(
604 "entropy channel count limit exceeded",
605 ));
606 }
607 let channel = EntropyChannelDescriptor::decode(&rec.payload, limits)?;
608 cost.entropy_payload += rec.payload.len() as u64;
609 channels.push(channel);
610 }
611 Some(RecordTag::Graph) => {
612 if program.is_some() {
613 return Err(Error::invalid_container("duplicate GRAPH record"));
614 }
615 let p = Program::decode(&rec.payload, limits)?;
616 cost.graph = rec.payload.len() as u64;
617 graph_payload = rec.payload.clone();
618 program = Some(p);
619 }
620 Some(RecordTag::ObservationIndex) => {
621 if observation_index.is_some() {
622 return Err(Error::invalid_container(
623 "duplicate OBSERVATION_INDEX record",
624 ));
625 }
626 let idx = ObservationIndex::decode(&rec.payload, limits)?;
627 cost.index = rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
628 index_records = 1;
629 observation_index = Some(idx);
630 }
631 Some(RecordTag::Directory) => {
632 if seek_directory.is_some() {
633 return Err(Error::invalid_container("duplicate DIRECTORY record"));
634 }
635 if !rec.is_optional() {
636 return Err(Error::invalid_container(
637 "DIRECTORY record must carry FLAG_OPTIONAL",
638 ));
639 }
640 if sites.len() != 1 {
641 return Err(Error::invalid_container(
642 "DIRECTORY record must be the first record",
643 ));
644 }
645 let dir = SeekDirectory::decode(&rec.payload, limits)?;
646 cost.directory = rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
647 directory_records = 1;
648 seek_directory = Some(dir);
649 }
650 Some(RecordTag::Integrity) => {
651 if source_sha256.is_some() {
652 return Err(Error::invalid_container("duplicate INTEGRITY record"));
653 }
654 if rec.payload.len() != 40 {
655 return Err(Error::invalid_container(
656 "INTEGRITY payload must be 40 bytes",
657 ));
658 }
659 let mut sha = [0u8; 32];
660 sha.copy_from_slice(&rec.payload[0..32]);
661 let len = u64::from_le_bytes([
662 rec.payload[32],
663 rec.payload[33],
664 rec.payload[34],
665 rec.payload[35],
666 rec.payload[36],
667 rec.payload[37],
668 rec.payload[38],
669 rec.payload[39],
670 ]);
671 source_sha256 = Some(sha);
672 source_len = Some(len);
673 cost.integrity = rec.payload.len() as u64;
674 }
675 Some(RecordTag::Trailer) => {
676 if rec.payload.len() != 20 {
677 return Err(Error::invalid_container("TRAILER payload must be 20 bytes"));
678 }
679 if rec.payload[12..20] != MAGIC {
680 return Err(Error::invalid_container("TRAILER magic mismatch"));
681 }
682 trailer_record_count = Some(u32::from_le_bytes([
683 rec.payload[0],
684 rec.payload[1],
685 rec.payload[2],
686 rec.payload[3],
687 ]));
688 cost.trailer = rec.payload.len() as u64;
689 saw_trailer = true;
690 }
691 Some(RecordTag::Checkpoint) => {
692 if checkpoints.is_some() {
693 return Err(Error::invalid_container("duplicate CHECKPOINT record"));
694 }
695 if !rec.is_optional() {
696 return Err(Error::invalid_container(
697 "CHECKPOINT record must carry FLAG_OPTIONAL",
698 ));
699 }
700 let cp = CheckpointTable::decode(&rec.payload, limits)?;
701 cost.checkpoints = rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
702 checkpoint_records = 1;
703 checkpoints = Some(cp);
704 }
705 Some(RecordTag::Residual) => {
707 if rec.is_optional() {
708 } else {
710 return Err(Error::unsupported_feature(format!(
711 "record class {} requires a universe this build does not implement",
712 rec.tag
713 )));
714 }
715 }
716 None => {
717 if rec.is_optional() {
718 } else {
720 return Err(Error::unsupported_feature(format!(
721 "unknown mandatory record tag {:#04x}",
722 rec.tag
723 )));
724 }
725 }
726 }
727 }
728
729 let universe =
730 universe.ok_or_else(|| Error::invalid_container("missing UNIVERSE record"))?;
731 let (class, basis) =
732 format.ok_or_else(|| Error::invalid_container("missing FORMAT record"))?;
733 let program = program.ok_or_else(|| Error::invalid_container("missing GRAPH record"))?;
734 let source_sha256 =
735 source_sha256.ok_or_else(|| Error::invalid_container("missing INTEGRITY record"))?;
736 let source_len =
737 source_len.ok_or_else(|| Error::invalid_container("missing INTEGRITY record"))?;
738 if !saw_trailer {
739 return Err(Error::invalid_container("missing TRAILER record"));
740 }
741 if let Some(n) = trailer_record_count
742 && n != records_seen
743 {
744 return Err(Error::invalid_container(format!(
745 "TRAILER declares {n} records but {records_seen} were read"
746 )));
747 }
748 if source_len != header.declared_source_len {
749 return Err(Error::integrity_mismatch(format!(
750 "INTEGRITY length {source_len} disagrees with header {}",
751 header.declared_source_len
752 )));
753 }
754
755 for (i, channel) in channels.iter().enumerate() {
759 let model = models.get(channel.model_id as usize).ok_or_else(|| {
760 Error::invalid_model(format!(
761 "entropy channel {i} references missing model {}",
762 channel.model_id
763 ))
764 })?;
765 if channel.scale_bits != model.scale_bits {
766 return Err(Error::invalid_model(format!(
767 "entropy channel {i} scale_bits {} disagrees with model {} scale_bits {}",
768 channel.scale_bits, channel.model_id, model.scale_bits
769 )));
770 }
771 }
772
773 let object_lens: Vec<u64> = objects.iter().map(|o| o.len()).collect();
776 let channel_lens: Vec<u64> = channels.iter().map(|c| c.decoded_length).collect();
777 let (predicted, coverage) = program.analyze(&object_lens, &channel_lens, limits)?;
778 if predicted != source_len {
779 return Err(Error::coverage_violation(format!(
780 "reconstruction program predicts {predicted} bytes but {source_len} were declared"
781 )));
782 }
783 coverage.validate(source_len)?;
784
785 if let Some(index) = &observation_index {
788 index.validate(&program, &object_lens, &channel_lens, limits)?;
789 }
790
791 if let Some(dir) = &seek_directory {
796 dir.validate(&sites, bytes.len() as u64, limits)?;
797 }
798
799 if let Some(cp) = &checkpoints {
803 cp.validate(
804 &program,
805 &graph_payload,
806 source_len,
807 &object_lens,
808 &channel_lens,
809 limits,
810 )?;
811 }
812
813 cost.record_framing = RECORD_OVERHEAD as u64
817 * (records_seen as u64 - index_records - directory_records - checkpoint_records);
818
819 Ok(ParsedDescriptor {
820 descriptor: Descriptor {
821 universe,
822 source_format: class,
823 format_basis: basis,
824 models,
825 channels,
826 objects,
827 program,
828 observation_index,
829 seek_directory: seek_directory.is_some(),
830 checkpoints,
831 source_sha256,
832 source_len,
833 },
834 cost,
835 universe_id: header.universe_id,
836 })
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use crate::SOURCE_FORMAT_OPAQUE;
844 use crate::dra::Op;
845 use crate::integrity::sha256;
846
847 fn sample(source: &[u8]) -> Descriptor {
848 Descriptor {
849 universe: UNIVERSE.to_string(),
850 source_format: SOURCE_FORMAT_OPAQUE,
851 format_basis: "opaque:test".to_string(),
852 models: vec![],
853 channels: vec![],
854 objects: vec![ObjectSource::Inline(source.to_vec())],
855 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
856 observation_index: None,
857 seek_directory: false,
858 checkpoints: None,
859 source_sha256: sha256(source),
860 source_len: source.len() as u64,
861 }
862 }
863
864 fn channel(model_id: u32, scale_bits: u8, decoded_length: u64) -> EntropyChannelDescriptor {
865 EntropyChannelDescriptor {
866 coder: crate::entropy::codec::CODER_ORDER0_BYTE_RANS,
867 coder_version: crate::entropy::codec::CODER_VERSION_1,
868 scale_bits,
869 lane_count: 1,
870 model_id,
871 symbol_count: decoded_length,
872 decoded_length,
873 initial_state: 1,
874 payload: vec![0u8; 4],
875 }
876 }
877
878 #[test]
879 fn model_and_channel_roundtrip() {
880 let payload = b"channel bytes";
881 let mut d = sample(payload);
882 d.models = vec![EntropyModel::uniform(8).unwrap()];
883 d.channels = vec![channel(0, 8, payload.len() as u64)];
884 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
885 let (bytes, cost) = d.serialize().unwrap();
886 assert_eq!(cost.total(), bytes.len() as u64);
887 assert!(cost.models > 0);
888 assert!(cost.entropy_payload > 0);
889 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
890 assert_eq!(parsed.descriptor, d);
891 assert_eq!(parsed.cost.total(), bytes.len() as u64);
892 }
893
894 #[test]
895 fn channel_with_missing_model_rejected() {
896 let mut d = sample(b"abc");
897 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
898 d.channels = vec![channel(3, 8, 3)];
899 let (bytes, _) = d.serialize().unwrap();
900 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
901 assert_eq!(e.class(), crate::ErrorClass::InvalidModel);
902 }
903
904 #[test]
905 fn channel_scale_mismatch_rejected() {
906 let mut d = sample(b"abc");
907 d.models = vec![EntropyModel::uniform(8).unwrap()];
908 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
909 d.channels = vec![channel(0, 12, 3)];
910 let (bytes, _) = d.serialize().unwrap();
911 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
912 assert_eq!(e.class(), crate::ErrorClass::InvalidModel);
913 }
914
915 #[test]
916 fn serialize_parse_roundtrip() {
917 let d = sample(b"hello, exact world");
918 let (bytes, cost) = d.serialize().unwrap();
919 assert_eq!(cost.total(), bytes.len() as u64);
920 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
921 assert_eq!(parsed.descriptor, d);
922 assert_eq!(parsed.cost.total(), bytes.len() as u64);
923 }
924
925 #[test]
926 fn trailing_bytes_after_trailer_rejected() {
927 let d = sample(b"abc");
928 let (mut bytes, _) = d.serialize().unwrap();
929 bytes.push(0);
930 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
931 assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
932 }
933
934 #[test]
935 fn declared_length_mismatch_rejected() {
936 let mut d = sample(b"abcdef");
938 d.source_len = 5;
939 let (bytes, _) = d.serialize().unwrap();
940 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
941 assert_eq!(e.class(), crate::ErrorClass::CoverageViolation);
942 }
943
944 fn replay_program() -> Program {
945 Program::new(vec![Op::DeflateReplay {
946 replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
947 source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
948 source_id: 0,
949 corrections_object: 0,
950 declared_output_len: 3,
951 }])
952 }
953
954 #[test]
955 fn plain_descriptor_declares_no_mandatory_features() {
956 assert_eq!(sample(b"abc").required_features(), 0);
957 }
958
959 #[cfg(feature = "deflate-replay")]
960 #[test]
961 fn replay_op_declares_mandatory_feature() {
962 let mut d = sample(b"abc");
963 d.program = replay_program();
964 assert_eq!(
965 d.required_features(),
966 crate::container::header::FEATURE_DEFLATE_REPLAY
967 );
968 let (bytes, _) = d.serialize().unwrap();
970 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
971 assert_eq!(
972 parsed.descriptor.required_features(),
973 crate::container::header::FEATURE_DEFLATE_REPLAY
974 );
975 }
976
977 #[cfg(not(feature = "deflate-replay"))]
978 #[test]
979 fn replay_descriptor_fails_closed_without_feature() {
980 let mut d = sample(b"abc");
981 d.program = replay_program();
982 let (bytes, _) = d.serialize().unwrap();
983 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
984 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
985 }
986
987 use crate::container::observation::{
988 DEP_NONE, DEP_OBJECT, ObservationDigest, ObservationSelector, OpEntry, SECTION_DIGESTS,
989 SECTION_OP_TABLE, SECTION_PDF_SELECTORS, SELECTOR_OBJECT,
990 };
991
992 fn indexed_descriptor() -> Descriptor {
995 let mut d = sample(b"");
996 d.objects = vec![ObjectSource::Inline(b"abc".to_vec())];
997 d.program = Program::new(vec![
998 Op::EmitObject { object_id: 0 },
999 Op::Inline {
1000 bytes: b"de".to_vec(),
1001 },
1002 ]);
1003 d.source_sha256 = sha256(b"abcde");
1004 d.source_len = 5;
1005 d.observation_index = Some(ObservationIndex {
1006 section_flags: SECTION_OP_TABLE | SECTION_PDF_SELECTORS | SECTION_DIGESTS,
1007 ops: vec![
1008 OpEntry {
1009 out_len: 3,
1010 dep_kind: DEP_OBJECT,
1011 dep_id: 0,
1012 },
1013 OpEntry {
1014 out_len: 2,
1015 dep_kind: DEP_NONE,
1016 dep_id: 0,
1017 },
1018 ],
1019 selectors: vec![ObservationSelector {
1020 kind: SELECTOR_OBJECT,
1021 number: 1,
1022 generation: 0,
1023 out_off: 0,
1024 out_len: 3,
1025 }],
1026 digests: vec![ObservationDigest {
1027 out_off: 3,
1028 out_len: 2,
1029 sha256: [7u8; 32],
1030 }],
1031 });
1032 d
1033 }
1034
1035 #[test]
1036 fn observation_index_roundtrip_and_charge() {
1037 let d = indexed_descriptor();
1038 assert_eq!(
1039 d.optional_features(),
1040 crate::container::header::FEATURE_OBSERVATION_INDEX
1041 );
1042 let (bytes, cost) = d.serialize().unwrap();
1043 assert_eq!(
1044 cost.total(),
1045 bytes.len() as u64,
1046 "cost must be the byte length"
1047 );
1048 assert!(
1049 cost.index > 0,
1050 "the index payload + framing must be charged"
1051 );
1052
1053 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1054 assert_eq!(parsed.descriptor, d);
1055 assert_eq!(parsed.cost.total(), bytes.len() as u64);
1056 assert!(parsed.cost.index > 0);
1057
1058 let plain = sample(b"nope");
1060 let (pbytes, pcost) = plain.serialize().unwrap();
1061 assert_eq!(pcost.total(), pbytes.len() as u64);
1062 assert_eq!(pcost.index, 0);
1063 assert_eq!(plain.optional_features(), 0);
1064 let reparsed = Descriptor::parse(&pbytes, Limits::DEFAULT).unwrap();
1065 assert!(reparsed.descriptor.observation_index.is_none());
1066 }
1067
1068 #[test]
1069 fn inconsistent_observation_index_is_rejected_on_parse() {
1070 let mut d = indexed_descriptor();
1072 d.observation_index.as_mut().unwrap().ops[0].out_len = 9;
1073 let (bytes, _) = d.serialize().unwrap();
1074 assert_eq!(
1075 Descriptor::parse(&bytes, Limits::DEFAULT)
1076 .unwrap_err()
1077 .class(),
1078 crate::ErrorClass::CoverageViolation
1079 );
1080
1081 let mut d = indexed_descriptor();
1082 let idx = d.observation_index.as_mut().unwrap();
1083 idx.ops[0].dep_kind = DEP_OBJECT;
1084 idx.ops[0].dep_id = 99;
1085 let (bytes, _) = d.serialize().unwrap();
1086 assert_eq!(
1087 Descriptor::parse(&bytes, Limits::DEFAULT)
1088 .unwrap_err()
1089 .class(),
1090 crate::ErrorClass::CoverageViolation
1091 );
1092
1093 let mut d = indexed_descriptor();
1094 let sel = &mut d.observation_index.as_mut().unwrap().selectors[0];
1095 sel.out_off = 4;
1096 sel.out_len = 9;
1097 let (bytes, _) = d.serialize().unwrap();
1098 assert_eq!(
1099 Descriptor::parse(&bytes, Limits::DEFAULT)
1100 .unwrap_err()
1101 .class(),
1102 crate::ErrorClass::CoverageViolation
1103 );
1104 }
1105
1106 fn seekable_descriptor() -> Descriptor {
1113 let mut d = indexed_descriptor();
1114 d.seek_directory = true;
1115 d
1116 }
1117
1118 fn rebuild_with_directory(bytes: &[u8], mut mutate: impl FnMut(&mut SeekDirectory)) -> Vec<u8> {
1123 use crate::container::record::{RecordReader, write_record};
1124 let header = &bytes[0..HEADER_LEN];
1125 let mut reader = RecordReader::new(bytes, HEADER_LEN, Limits::DEFAULT);
1126 let mut records = Vec::new();
1127 while let Some(r) = reader.next_record().unwrap() {
1128 records.push(r);
1129 }
1130 let mut out = header.to_vec();
1131 for r in &records {
1132 if r.tag == RecordTag::Directory as u8 {
1133 let mut dir = SeekDirectory::decode(&r.payload, Limits::DEFAULT).unwrap();
1134 mutate(&mut dir);
1135 write_record(&mut out, r.tag, r.flags, &dir.encode().unwrap()).unwrap();
1136 } else {
1137 write_record(&mut out, r.tag, r.flags, &r.payload).unwrap();
1138 }
1139 }
1140 out
1141 }
1142
1143 #[test]
1144 fn seek_directory_roundtrips_materializes_and_charges() {
1145 let d = seekable_descriptor();
1146 assert_eq!(
1147 d.optional_features(),
1148 crate::container::header::FEATURE_OBSERVATION_INDEX
1149 | crate::container::header::FEATURE_SEEK_DIRECTORY
1150 );
1151 let (bytes, cost) = d.serialize().unwrap();
1152 assert_eq!(cost.total(), bytes.len() as u64, "cost must be the length");
1153 assert!(
1154 cost.directory > 0,
1155 "the directory payload + framing is charged"
1156 );
1157
1158 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1159 assert_eq!(parsed.descriptor, d, "seekable descriptor must round-trip");
1160 assert!(parsed.descriptor.seek_directory);
1161 assert_eq!(parsed.cost.total(), bytes.len() as u64);
1162 assert_eq!(parsed.cost.directory, cost.directory);
1163
1164 let out = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT)
1166 .unwrap()
1167 .0;
1168 assert_eq!(out, b"abcde");
1169 }
1170
1171 #[test]
1172 fn directory_is_the_first_record_and_is_optional() {
1173 let (bytes, _) = seekable_descriptor().serialize().unwrap();
1174 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1175 let first = r.next_record().unwrap().unwrap();
1176 assert_eq!(first.tag, RecordTag::Directory as u8);
1177 assert!(
1178 first.is_optional(),
1179 "the directory must carry FLAG_OPTIONAL"
1180 );
1181 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1183 r.next_record().unwrap().unwrap();
1184 assert_eq!(
1185 r.position(),
1186 HEADER_LEN + RECORD_OVERHEAD + first.payload.len(),
1187 "the next record must begin right after the directory"
1188 );
1189 }
1190
1191 #[test]
1192 fn non_seekable_descriptor_has_no_directory_cost() {
1193 let d = sample(b"no directory here");
1194 let (bytes, cost) = d.serialize().unwrap();
1195 assert_eq!(cost.directory, 0);
1196 assert_eq!(cost.total(), bytes.len() as u64);
1197 assert_eq!(d.optional_features(), 0);
1198 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1201 assert_eq!(
1202 r.next_record().unwrap().unwrap().tag,
1203 RecordTag::Universe as u8
1204 );
1205 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1206 assert!(!parsed.descriptor.seek_directory);
1207 assert_eq!(parsed.cost.directory, 0);
1208 }
1209
1210 #[test]
1211 fn seek_directory_without_index_is_rejected() {
1212 let mut d = sample(b"abc");
1213 d.seek_directory = true;
1214 assert_eq!(
1215 d.serialize().unwrap_err().class(),
1216 crate::ErrorClass::InvalidContainer
1217 );
1218 }
1219
1220 #[test]
1221 fn corrupted_directory_payload_is_rejected() {
1222 let (mut bytes, _) = seekable_descriptor().serialize().unwrap();
1223 bytes[HEADER_LEN + 8] ^= 0x01;
1226 assert_eq!(
1227 Descriptor::parse(&bytes, Limits::DEFAULT)
1228 .unwrap_err()
1229 .class(),
1230 crate::ErrorClass::InvalidContainer
1231 );
1232 }
1233
1234 #[test]
1235 fn lying_directory_is_rejected_on_parse() {
1236 let (bytes, _) = seekable_descriptor().serialize().unwrap();
1237 Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1239
1240 let lying = rebuild_with_directory(&bytes, |dir| dir.entries[1].offset += 1);
1242 assert_eq!(
1243 Descriptor::parse(&lying, Limits::DEFAULT)
1244 .unwrap_err()
1245 .class(),
1246 crate::ErrorClass::InvalidContainer
1247 );
1248
1249 let lying = rebuild_with_directory(&bytes, |dir| dir.classes[0].count += 1);
1251 assert_eq!(
1252 Descriptor::parse(&lying, Limits::DEFAULT)
1253 .unwrap_err()
1254 .class(),
1255 crate::ErrorClass::InvalidContainer
1256 );
1257
1258 let mut reader = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1261 let mut records = Vec::new();
1262 while let Some(r) = reader.next_record().unwrap() {
1263 records.push(r);
1264 }
1265 let mut out = bytes[0..HEADER_LEN].to_vec();
1266 for r in &records {
1267 let flags = if r.tag == RecordTag::Directory as u8 {
1268 0
1269 } else {
1270 r.flags
1271 };
1272 crate::container::record::write_record(&mut out, r.tag, flags, &r.payload).unwrap();
1273 }
1274 assert_eq!(
1275 Descriptor::parse(&out, Limits::DEFAULT)
1276 .unwrap_err()
1277 .class(),
1278 crate::ErrorClass::InvalidContainer
1279 );
1280 }
1281
1282 fn checkpointed_descriptor() -> Descriptor {
1288 let mut d = seekable_descriptor();
1289 let object_lens: Vec<u64> = d.objects.iter().map(|o| o.len()).collect();
1290 let channel_lens: Vec<u64> = d.channels.iter().map(|c| c.decoded_length).collect();
1291 let table = CheckpointTable::from_program(
1292 &d.program,
1293 &object_lens,
1294 &channel_lens,
1295 d.source_len,
1296 Limits::DEFAULT,
1297 )
1298 .unwrap();
1299 d.checkpoints = Some(table);
1300 d
1301 }
1302
1303 fn rebuild_with_checkpoint(
1308 bytes: &[u8],
1309 mut mutate: impl FnMut(&mut CheckpointTable),
1310 ) -> Vec<u8> {
1311 use crate::container::record::{RecordReader, write_record};
1312 let header = &bytes[0..HEADER_LEN];
1313 let mut reader = RecordReader::new(bytes, HEADER_LEN, Limits::DEFAULT);
1314 let mut records = Vec::new();
1315 while let Some(r) = reader.next_record().unwrap() {
1316 records.push(r);
1317 }
1318 let mut out = header.to_vec();
1319 for r in &records {
1320 if r.tag == RecordTag::Checkpoint as u8 {
1321 let mut cp = CheckpointTable::decode(&r.payload, Limits::DEFAULT).unwrap();
1322 mutate(&mut cp);
1323 write_record(&mut out, r.tag, r.flags, &cp.encode().unwrap()).unwrap();
1324 } else {
1325 write_record(&mut out, r.tag, r.flags, &r.payload).unwrap();
1326 }
1327 }
1328 out
1329 }
1330
1331 #[test]
1332 fn checkpoint_roundtrips_materializes_and_charges() {
1333 let d = checkpointed_descriptor();
1334 assert_eq!(
1335 d.optional_features(),
1336 crate::container::header::FEATURE_OBSERVATION_INDEX
1337 | crate::container::header::FEATURE_SEEK_DIRECTORY
1338 | crate::container::header::FEATURE_CHECKPOINTS
1339 );
1340 let (bytes, cost) = d.serialize().unwrap();
1341 assert_eq!(cost.total(), bytes.len() as u64, "cost must be the length");
1342 assert!(
1343 cost.checkpoints > 0,
1344 "the checkpoint payload + framing is charged"
1345 );
1346
1347 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1348 assert_eq!(
1349 parsed.descriptor, d,
1350 "checkpointed descriptor must round-trip"
1351 );
1352 assert!(parsed.descriptor.checkpoints.is_some());
1353 assert_eq!(parsed.cost.total(), bytes.len() as u64);
1354 assert_eq!(parsed.cost.checkpoints, cost.checkpoints);
1355
1356 let out = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT)
1358 .unwrap()
1359 .0;
1360 assert_eq!(out, b"abcde");
1361 }
1362
1363 #[test]
1364 fn checkpoint_without_directory_is_rejected() {
1365 let mut d = checkpointed_descriptor();
1366 d.seek_directory = false;
1367 assert_eq!(
1368 d.serialize().unwrap_err().class(),
1369 crate::ErrorClass::InvalidContainer
1370 );
1371 }
1372
1373 #[test]
1374 fn absent_checkpoint_has_no_cost_and_no_feature_bit() {
1375 let d = seekable_descriptor();
1376 assert!(d.checkpoints.is_none());
1377 let (bytes, cost) = d.serialize().unwrap();
1378 assert_eq!(cost.checkpoints, 0);
1379 assert_eq!(cost.total(), bytes.len() as u64);
1380 assert_eq!(
1381 d.optional_features() & crate::container::header::FEATURE_CHECKPOINTS,
1382 0
1383 );
1384 }
1385
1386 #[test]
1387 fn corrupt_or_non_optional_checkpoint_is_rejected() {
1388 let (mut bytes, _) = checkpointed_descriptor().serialize().unwrap();
1389 let mut reader =
1391 crate::container::record::RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1392 let mut at = None;
1393 loop {
1394 let p = reader.position();
1395 match reader.next_record().unwrap() {
1396 Some(r) if r.tag == RecordTag::Checkpoint as u8 => {
1397 at = Some(p + 8);
1398 break;
1399 }
1400 Some(_) => {}
1401 None => break,
1402 }
1403 }
1404 bytes[at.expect("a checkpoint record")] ^= 0x01;
1405 assert_eq!(
1406 Descriptor::parse(&bytes, Limits::DEFAULT)
1407 .unwrap_err()
1408 .class(),
1409 crate::ErrorClass::InvalidContainer
1410 );
1411
1412 let (bytes, _) = checkpointed_descriptor().serialize().unwrap();
1415 let mut reader =
1416 crate::container::record::RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1417 let mut records = Vec::new();
1418 while let Some(r) = reader.next_record().unwrap() {
1419 records.push(r);
1420 }
1421 let mut out = bytes[0..HEADER_LEN].to_vec();
1422 for r in &records {
1423 let flags = if r.tag == RecordTag::Checkpoint as u8 {
1424 0
1425 } else {
1426 r.flags
1427 };
1428 crate::container::record::write_record(&mut out, r.tag, flags, &r.payload).unwrap();
1429 }
1430 assert_eq!(
1431 Descriptor::parse(&out, Limits::DEFAULT)
1432 .unwrap_err()
1433 .class(),
1434 crate::ErrorClass::InvalidContainer
1435 );
1436 }
1437
1438 #[test]
1439 fn lying_checkpoint_is_rejected_on_parse() {
1440 let (bytes, _) = checkpointed_descriptor().serialize().unwrap();
1441 Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1442
1443 let lying = rebuild_with_checkpoint(&bytes, |cp| {
1445 cp.entries[0].out_len = 4;
1446 cp.entries[1].out_start = 4;
1447 cp.entries[1].out_len = 1;
1448 });
1449 assert_eq!(
1450 Descriptor::parse(&lying, Limits::DEFAULT)
1451 .unwrap_err()
1452 .class(),
1453 crate::ErrorClass::CoverageViolation
1454 );
1455
1456 let lying = rebuild_with_checkpoint(&bytes, |cp| cp.graph_crc32c ^= 0xFFFF_FFFF);
1458 assert_eq!(
1459 Descriptor::parse(&lying, Limits::DEFAULT)
1460 .unwrap_err()
1461 .class(),
1462 crate::ErrorClass::InvalidContainer
1463 );
1464
1465 let lying = rebuild_with_checkpoint(&bytes, |cp| cp.source_len += 1);
1467 assert_eq!(
1468 Descriptor::parse(&lying, Limits::DEFAULT)
1469 .unwrap_err()
1470 .class(),
1471 crate::ErrorClass::IntegrityMismatch
1472 );
1473 }
1474}