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