1use crate::EXACTNESS_PROFILE_EXACT_BYTES;
9use crate::accounting::CostBreakdown;
10use crate::container::directory::{
11 DirectoryEntry, RecordSite, SEEK_DIRECTORY_ALL_SECTIONS, SeekDirectory,
12};
13use crate::container::header::{HEADER_LEN, Header, MAGIC};
14use crate::container::observation::ObservationIndex;
15use crate::container::record::{FLAG_OPTIONAL, RECORD_OVERHEAD, RecordReader, RecordTag};
16use crate::dra::Program;
17use crate::entropy::codec::EntropyChannelDescriptor;
18use crate::entropy::model::EntropyModel;
19use crate::error::{Error, Result};
20use crate::integrity::sha256;
21use crate::limits::Limits;
22use crate::store::Id;
23
24pub const EXTERNAL_REF_PAYLOAD_LEN: usize = 40;
26
27pub 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";
41
42pub fn universe_id_from_str(universe: &str) -> [u8; 16] {
44 let full = sha256(universe.as_bytes());
45 let mut id = [0u8; 16];
46 id.copy_from_slice(&full[0..16]);
47 id
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ObjectSource {
57 Inline(Vec<u8>),
59 External { id: Id, len: u64 },
62}
63
64impl ObjectSource {
65 pub fn len(&self) -> u64 {
67 match self {
68 ObjectSource::Inline(b) => b.len() as u64,
69 ObjectSource::External { len, .. } => *len,
70 }
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.len() == 0
76 }
77
78 pub fn as_inline(&self) -> Option<&[u8]> {
80 match self {
81 ObjectSource::Inline(b) => Some(b),
82 ObjectSource::External { .. } => None,
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Descriptor {
90 pub universe: String,
92 pub source_format: u8,
94 pub format_basis: String,
96 pub models: Vec<EntropyModel>,
98 pub channels: Vec<EntropyChannelDescriptor>,
100 pub objects: Vec<ObjectSource>,
102 pub program: Program,
104 pub observation_index: Option<ObservationIndex>,
110 pub seek_directory: bool,
117 pub source_sha256: [u8; 32],
119 pub source_len: u64,
121}
122
123#[derive(Debug, Clone)]
125pub struct ParsedDescriptor {
126 pub descriptor: Descriptor,
128 pub cost: CostBreakdown,
130 pub universe_id: [u8; 16],
132}
133
134struct PendingRecord {
136 tag: RecordTag,
137 flags: u8,
138 payload: Vec<u8>,
139}
140
141impl PendingRecord {
142 fn new(tag: RecordTag, flags: u8, payload: Vec<u8>) -> Self {
143 PendingRecord {
144 tag,
145 flags,
146 payload,
147 }
148 }
149}
150
151impl Descriptor {
152 pub fn header(&self) -> Header {
154 let mut header = Header::new(
155 universe_id_from_str(&self.universe),
156 self.source_len,
157 EXACTNESS_PROFILE_EXACT_BYTES,
158 self.source_format,
159 );
160 header.mandatory_features = self.required_features();
161 header.optional_features = self.optional_features();
162 header
163 }
164
165 pub fn required_features(&self) -> u32 {
172 let mut bits = 0u32;
173 for op in &self.program.ops {
174 if matches!(op, crate::dra::Op::DeflateReplay { .. }) {
175 bits |= crate::container::header::FEATURE_DEFLATE_REPLAY;
176 }
177 }
178 if self
179 .objects
180 .iter()
181 .any(|o| matches!(o, ObjectSource::External { .. }))
182 {
183 bits |= crate::container::header::FEATURE_EXTERNAL_OBJECTS;
184 }
185 bits
186 }
187
188 pub fn optional_features(&self) -> u32 {
195 let mut bits = 0u32;
196 if self.observation_index.is_some() {
197 bits |= crate::container::header::FEATURE_OBSERVATION_INDEX;
198 }
199 if self.seek_directory {
200 bits |= crate::container::header::FEATURE_SEEK_DIRECTORY;
201 }
202 bits
203 }
204
205 pub fn serialize(&self) -> Result<(Vec<u8>, CostBreakdown)> {
214 if self.seek_directory && self.observation_index.is_none() {
215 return Err(Error::invalid_container(
216 "a seek directory requires an observation index",
217 ));
218 }
219
220 let mut cost = CostBreakdown {
221 header: HEADER_LEN as u64,
222 ..Default::default()
223 };
224
225 let mut pending: Vec<PendingRecord> = Vec::new();
227
228 pending.push(PendingRecord::new(
230 RecordTag::Universe,
231 0,
232 self.universe.as_bytes().to_vec(),
233 ));
234 cost.universe = self.universe.len() as u64;
235
236 let basis = self.format_basis.as_bytes();
238 let basis_len = u32::try_from(basis.len())
239 .map_err(|_| Error::resource_limit("format basis too long"))?;
240 let mut fmt = Vec::with_capacity(5 + basis.len());
241 fmt.push(self.source_format);
242 fmt.extend_from_slice(&basis_len.to_le_bytes());
243 fmt.extend_from_slice(basis);
244 cost.format = fmt.len() as u64;
245 pending.push(PendingRecord::new(RecordTag::Format, 0, fmt));
246
247 for model in &self.models {
249 let encoded = model.encode()?;
250 cost.models += encoded.len() as u64;
251 pending.push(PendingRecord::new(RecordTag::Model, 0, encoded));
252 }
253
254 for channel in &self.channels {
256 let encoded = channel.encode()?;
257 cost.entropy_payload += encoded.len() as u64;
258 pending.push(PendingRecord::new(RecordTag::EntropyChannel, 0, encoded));
259 }
260
261 for obj in &self.objects {
264 match obj {
265 ObjectSource::Inline(bytes) => {
266 cost.objects += bytes.len() as u64;
267 pending.push(PendingRecord::new(RecordTag::Object, 0, bytes.clone()));
268 }
269 ObjectSource::External { id, len } => {
270 let mut payload = Vec::with_capacity(EXTERNAL_REF_PAYLOAD_LEN);
271 payload.extend_from_slice(id.as_bytes());
272 payload.extend_from_slice(&len.to_le_bytes());
273 cost.external_refs += payload.len() as u64;
274 pending.push(PendingRecord::new(RecordTag::ExternalRef, 0, payload));
275 }
276 }
277 }
278
279 let graph = self.program.encode()?;
281 cost.graph = graph.len() as u64;
282 pending.push(PendingRecord::new(RecordTag::Graph, 0, graph));
283
284 let mut index_records: u64 = 0;
287 if let Some(index) = &self.observation_index {
288 let payload = index.encode()?;
289 index_records = 1;
290 cost.index = payload.len() as u64 + RECORD_OVERHEAD as u64;
291 pending.push(PendingRecord::new(
292 RecordTag::ObservationIndex,
293 FLAG_OPTIONAL,
294 payload,
295 ));
296 }
297
298 let mut integ = Vec::with_capacity(40);
300 integ.extend_from_slice(&self.source_sha256);
301 integ.extend_from_slice(&self.source_len.to_le_bytes());
302 cost.integrity = integ.len() as u64;
303 pending.push(PendingRecord::new(RecordTag::Integrity, 0, integ));
304
305 let mut directory_payload: Option<Vec<u8>> = None;
308 let mut directory_records: u64 = 0;
309 if self.seek_directory {
310 let channel_lengths: Vec<u64> =
311 self.channels.iter().map(|c| c.decoded_length).collect();
312
313 let mut entries: Vec<DirectoryEntry> = Vec::with_capacity(pending.len() + 2);
317 entries.push(DirectoryEntry {
318 tag: RecordTag::Directory as u8,
319 offset: HEADER_LEN as u64,
320 payload_len: 0,
321 });
322 for rec in &pending {
323 let payload_len = u32::try_from(rec.payload.len())
324 .map_err(|_| Error::resource_limit("record payload exceeds u32"))?;
325 entries.push(DirectoryEntry {
326 tag: rec.tag as u8,
327 offset: 0,
328 payload_len,
329 });
330 }
331 entries.push(DirectoryEntry {
332 tag: RecordTag::Trailer as u8,
333 offset: 0,
334 payload_len: 20,
335 });
336 let classes = crate::container::directory::class_index(&entries);
337
338 let mut dir = SeekDirectory {
342 section_flags: SEEK_DIRECTORY_ALL_SECTIONS,
343 entries,
344 classes,
345 channel_lengths,
346 };
347 let dir_payload_len = dir.encode()?.len();
348 dir.entries[0].payload_len = u32::try_from(dir_payload_len)
349 .map_err(|_| Error::resource_limit("seek directory payload exceeds u32"))?;
350
351 let mut off = (HEADER_LEN + RECORD_OVERHEAD + dir_payload_len) as u64;
352 let last = dir.entries.len() - 1;
353 for entry in &mut dir.entries[1..last] {
354 entry.offset = off;
355 off = off
356 .checked_add(RECORD_OVERHEAD as u64)
357 .and_then(|v| v.checked_add(u64::from(entry.payload_len)))
358 .ok_or_else(|| Error::invalid_container("seek directory offset overflow"))?;
359 }
360 dir.entries[last].offset = off;
361
362 let payload = dir.encode()?;
363 debug_assert_eq!(payload.len(), dir_payload_len);
364 cost.directory = dir_payload_len as u64 + RECORD_OVERHEAD as u64;
365 directory_records = 1;
366 directory_payload = Some(payload);
367 }
368
369 let mut out = Vec::new();
371 out.extend_from_slice(&self.header().encode());
372
373 if let Some(payload) = &directory_payload {
374 crate::container::record::write_record(
375 &mut out,
376 RecordTag::Directory as u8,
377 FLAG_OPTIONAL,
378 payload,
379 )?;
380 }
381 for rec in &pending {
382 crate::container::record::write_record(
383 &mut out,
384 rec.tag as u8,
385 rec.flags,
386 &rec.payload,
387 )?;
388 }
389
390 let total_records = pending.len() as u64 + directory_records + 1;
393 let total_records =
394 u32::try_from(total_records).map_err(|_| Error::resource_limit("too many records"))?;
395 let payload_bytes = (out.len() - HEADER_LEN) as u64;
396 let mut trailer = Vec::with_capacity(20);
397 trailer.extend_from_slice(&total_records.to_le_bytes());
398 trailer.extend_from_slice(&payload_bytes.to_le_bytes());
399 trailer.extend_from_slice(&MAGIC);
400 crate::container::record::write_record(&mut out, RecordTag::Trailer as u8, 0, &trailer)?;
401 cost.trailer = trailer.len() as u64;
402
403 cost.record_framing =
409 RECORD_OVERHEAD as u64 * (u64::from(total_records) - index_records - directory_records);
410
411 debug_assert_eq!(cost.total(), out.len() as u64);
412 Ok((out, cost))
413 }
414
415 pub fn parse(bytes: &[u8], limits: Limits) -> Result<ParsedDescriptor> {
422 if bytes.len() as u64 > limits.max_input_bytes {
423 return Err(Error::resource_limit(
424 "input exceeds configured input limit",
425 ));
426 }
427 let header = Header::decode(bytes)?;
428 if !header.source_format_supported() {
429 return Err(Error::unsupported_feature(format!(
430 "source format class {} has no adapter in this build",
431 header.source_format
432 )));
433 }
434
435 let mut cost = CostBreakdown {
436 header: HEADER_LEN as u64,
437 ..Default::default()
438 };
439
440 let mut reader = RecordReader::new(bytes, HEADER_LEN, limits);
441 let mut universe: Option<String> = None;
442 let mut format: Option<(u8, String)> = None;
443 let mut models: Vec<EntropyModel> = Vec::new();
444 let mut channels: Vec<EntropyChannelDescriptor> = Vec::new();
445 let mut objects: Vec<ObjectSource> = Vec::new();
446 let mut program: Option<Program> = None;
447 let mut observation_index: Option<ObservationIndex> = None;
448 let mut source_sha256: Option<[u8; 32]> = None;
449 let mut source_len: Option<u64> = None;
450 let mut saw_trailer = false;
451 let mut trailer_record_count: Option<u32> = None;
452 let mut records_seen: u32 = 0;
453 let mut index_records: u64 = 0;
454 let mut directory_records: u64 = 0;
455 let mut seek_directory: Option<SeekDirectory> = None;
456 let mut sites: Vec<RecordSite> = Vec::new();
457
458 while let Some(rec) = reader.next_record()? {
459 records_seen += 1;
460 let site = RecordSite {
461 tag: rec.tag,
462 offset: reader.position() as u64
463 - (RECORD_OVERHEAD as u64 + rec.payload.len() as u64),
464 payload_len: u32::try_from(rec.payload.len())
465 .map_err(|_| Error::resource_limit("record payload exceeds u32"))?,
466 };
467 sites.push(site);
468 if saw_trailer {
469 return Err(Error::invalid_container("record found after TRAILER"));
470 }
471 match RecordTag::from_u8(rec.tag) {
472 Some(RecordTag::Universe) => {
473 if universe.is_some() {
474 return Err(Error::invalid_container("duplicate UNIVERSE record"));
475 }
476 let payload_len = rec.payload.len();
477 let s = String::from_utf8(rec.payload)
478 .map_err(|_| Error::invalid_container("universe is not valid UTF-8"))?;
479 if universe_id_from_str(&s) != header.universe_id {
480 return Err(Error::invalid_container(
481 "universe declaration does not match its header identifier",
482 ));
483 }
484 universe = Some(s);
485 cost.universe = payload_len as u64;
486 }
487 Some(RecordTag::Format) => {
488 if format.is_some() {
489 return Err(Error::invalid_container("duplicate FORMAT record"));
490 }
491 if rec.payload.len() < 5 {
492 return Err(Error::invalid_container("truncated FORMAT payload"));
493 }
494 let class = rec.payload[0];
495 let blen = u32::from_le_bytes([
496 rec.payload[1],
497 rec.payload[2],
498 rec.payload[3],
499 rec.payload[4],
500 ]);
501 let blen = blen as usize;
502 if rec.payload.len() != 5 + blen {
503 return Err(Error::invalid_container("FORMAT payload length mismatch"));
504 }
505 let basis = String::from_utf8(rec.payload[5..].to_vec())
506 .map_err(|_| Error::invalid_container("format basis is not UTF-8"))?;
507 if class != header.source_format {
508 return Err(Error::invalid_container(
509 "FORMAT class disagrees with header source_format",
510 ));
511 }
512 format = Some((class, basis));
513 cost.format = rec.payload.len() as u64;
514 }
515 Some(RecordTag::Object) => {
516 if objects.len() as u32 >= limits.max_object_count {
517 return Err(Error::resource_limit("object count limit exceeded"));
518 }
519 cost.objects += rec.payload.len() as u64;
520 objects.push(ObjectSource::Inline(rec.payload));
521 }
522 Some(RecordTag::ExternalRef) => {
523 if objects.len() as u32 >= limits.max_object_count {
524 return Err(Error::resource_limit("object count limit exceeded"));
525 }
526 if rec.payload.len() != EXTERNAL_REF_PAYLOAD_LEN {
527 return Err(Error::invalid_container(
528 "EXTERNAL_REF payload must be 40 bytes",
529 ));
530 }
531 let mut id = [0u8; 32];
532 id.copy_from_slice(&rec.payload[0..32]);
533 let len = u64::from_le_bytes([
534 rec.payload[32],
535 rec.payload[33],
536 rec.payload[34],
537 rec.payload[35],
538 rec.payload[36],
539 rec.payload[37],
540 rec.payload[38],
541 rec.payload[39],
542 ]);
543 cost.external_refs += rec.payload.len() as u64;
544 objects.push(ObjectSource::External {
545 id: Id::from_bytes(id),
546 len,
547 });
548 }
549 Some(RecordTag::Model) => {
550 if models.len() as u32 >= limits.max_model_count {
551 return Err(Error::resource_limit("entropy model count limit exceeded"));
552 }
553 if rec.payload.len() as u32 > limits.max_entropy_model_bytes {
554 return Err(Error::resource_limit(format!(
555 "entropy model payload {} exceeds limit {}",
556 rec.payload.len(),
557 limits.max_entropy_model_bytes
558 )));
559 }
560 let model = EntropyModel::decode(&rec.payload)?;
561 cost.models += rec.payload.len() as u64;
562 models.push(model);
563 }
564 Some(RecordTag::EntropyChannel) => {
565 if channels.len() as u32 >= limits.max_channel_count {
566 return Err(Error::resource_limit(
567 "entropy channel count limit exceeded",
568 ));
569 }
570 let channel = EntropyChannelDescriptor::decode(&rec.payload, limits)?;
571 cost.entropy_payload += rec.payload.len() as u64;
572 channels.push(channel);
573 }
574 Some(RecordTag::Graph) => {
575 if program.is_some() {
576 return Err(Error::invalid_container("duplicate GRAPH record"));
577 }
578 let p = Program::decode(&rec.payload, limits)?;
579 cost.graph = rec.payload.len() as u64;
580 program = Some(p);
581 }
582 Some(RecordTag::ObservationIndex) => {
583 if observation_index.is_some() {
584 return Err(Error::invalid_container(
585 "duplicate OBSERVATION_INDEX record",
586 ));
587 }
588 let idx = ObservationIndex::decode(&rec.payload, limits)?;
589 cost.index = rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
590 index_records = 1;
591 observation_index = Some(idx);
592 }
593 Some(RecordTag::Directory) => {
594 if seek_directory.is_some() {
595 return Err(Error::invalid_container("duplicate DIRECTORY record"));
596 }
597 if !rec.is_optional() {
598 return Err(Error::invalid_container(
599 "DIRECTORY record must carry FLAG_OPTIONAL",
600 ));
601 }
602 if sites.len() != 1 {
603 return Err(Error::invalid_container(
604 "DIRECTORY record must be the first record",
605 ));
606 }
607 let dir = SeekDirectory::decode(&rec.payload, limits)?;
608 cost.directory = rec.payload.len() as u64 + RECORD_OVERHEAD as u64;
609 directory_records = 1;
610 seek_directory = Some(dir);
611 }
612 Some(RecordTag::Integrity) => {
613 if source_sha256.is_some() {
614 return Err(Error::invalid_container("duplicate INTEGRITY record"));
615 }
616 if rec.payload.len() != 40 {
617 return Err(Error::invalid_container(
618 "INTEGRITY payload must be 40 bytes",
619 ));
620 }
621 let mut sha = [0u8; 32];
622 sha.copy_from_slice(&rec.payload[0..32]);
623 let len = u64::from_le_bytes([
624 rec.payload[32],
625 rec.payload[33],
626 rec.payload[34],
627 rec.payload[35],
628 rec.payload[36],
629 rec.payload[37],
630 rec.payload[38],
631 rec.payload[39],
632 ]);
633 source_sha256 = Some(sha);
634 source_len = Some(len);
635 cost.integrity = rec.payload.len() as u64;
636 }
637 Some(RecordTag::Trailer) => {
638 if rec.payload.len() != 20 {
639 return Err(Error::invalid_container("TRAILER payload must be 20 bytes"));
640 }
641 if rec.payload[12..20] != MAGIC {
642 return Err(Error::invalid_container("TRAILER magic mismatch"));
643 }
644 trailer_record_count = Some(u32::from_le_bytes([
645 rec.payload[0],
646 rec.payload[1],
647 rec.payload[2],
648 rec.payload[3],
649 ]));
650 cost.trailer = rec.payload.len() as u64;
651 saw_trailer = true;
652 }
653 Some(RecordTag::Residual) | Some(RecordTag::Checkpoint) => {
655 if rec.is_optional() {
656 } else {
658 return Err(Error::unsupported_feature(format!(
659 "record class {} requires a universe this build does not implement",
660 rec.tag
661 )));
662 }
663 }
664 None => {
665 if rec.is_optional() {
666 } else {
668 return Err(Error::unsupported_feature(format!(
669 "unknown mandatory record tag {:#04x}",
670 rec.tag
671 )));
672 }
673 }
674 }
675 }
676
677 let universe =
678 universe.ok_or_else(|| Error::invalid_container("missing UNIVERSE record"))?;
679 let (class, basis) =
680 format.ok_or_else(|| Error::invalid_container("missing FORMAT record"))?;
681 let program = program.ok_or_else(|| Error::invalid_container("missing GRAPH record"))?;
682 let source_sha256 =
683 source_sha256.ok_or_else(|| Error::invalid_container("missing INTEGRITY record"))?;
684 let source_len =
685 source_len.ok_or_else(|| Error::invalid_container("missing INTEGRITY record"))?;
686 if !saw_trailer {
687 return Err(Error::invalid_container("missing TRAILER record"));
688 }
689 if let Some(n) = trailer_record_count
690 && n != records_seen
691 {
692 return Err(Error::invalid_container(format!(
693 "TRAILER declares {n} records but {records_seen} were read"
694 )));
695 }
696 if source_len != header.declared_source_len {
697 return Err(Error::integrity_mismatch(format!(
698 "INTEGRITY length {source_len} disagrees with header {}",
699 header.declared_source_len
700 )));
701 }
702
703 for (i, channel) in channels.iter().enumerate() {
707 let model = models.get(channel.model_id as usize).ok_or_else(|| {
708 Error::invalid_model(format!(
709 "entropy channel {i} references missing model {}",
710 channel.model_id
711 ))
712 })?;
713 if channel.scale_bits != model.scale_bits {
714 return Err(Error::invalid_model(format!(
715 "entropy channel {i} scale_bits {} disagrees with model {} scale_bits {}",
716 channel.scale_bits, channel.model_id, model.scale_bits
717 )));
718 }
719 }
720
721 let object_lens: Vec<u64> = objects.iter().map(|o| o.len()).collect();
724 let channel_lens: Vec<u64> = channels.iter().map(|c| c.decoded_length).collect();
725 let (predicted, coverage) = program.analyze(&object_lens, &channel_lens, limits)?;
726 if predicted != source_len {
727 return Err(Error::coverage_violation(format!(
728 "reconstruction program predicts {predicted} bytes but {source_len} were declared"
729 )));
730 }
731 coverage.validate(source_len)?;
732
733 if let Some(index) = &observation_index {
736 index.validate(&program, &object_lens, &channel_lens, limits)?;
737 }
738
739 if let Some(dir) = &seek_directory {
744 dir.validate(&sites, bytes.len() as u64, limits)?;
745 }
746
747 cost.record_framing =
751 RECORD_OVERHEAD as u64 * (records_seen as u64 - index_records - directory_records);
752
753 Ok(ParsedDescriptor {
754 descriptor: Descriptor {
755 universe,
756 source_format: class,
757 format_basis: basis,
758 models,
759 channels,
760 objects,
761 program,
762 observation_index,
763 seek_directory: seek_directory.is_some(),
764 source_sha256,
765 source_len,
766 },
767 cost,
768 universe_id: header.universe_id,
769 })
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776 use crate::SOURCE_FORMAT_OPAQUE;
777 use crate::dra::Op;
778 use crate::integrity::sha256;
779
780 fn sample(source: &[u8]) -> Descriptor {
781 Descriptor {
782 universe: UNIVERSE.to_string(),
783 source_format: SOURCE_FORMAT_OPAQUE,
784 format_basis: "opaque:test".to_string(),
785 models: vec![],
786 channels: vec![],
787 objects: vec![ObjectSource::Inline(source.to_vec())],
788 program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
789 observation_index: None,
790 seek_directory: false,
791 source_sha256: sha256(source),
792 source_len: source.len() as u64,
793 }
794 }
795
796 fn channel(model_id: u32, scale_bits: u8, decoded_length: u64) -> EntropyChannelDescriptor {
797 EntropyChannelDescriptor {
798 coder: crate::entropy::codec::CODER_ORDER0_BYTE_RANS,
799 coder_version: crate::entropy::codec::CODER_VERSION_1,
800 scale_bits,
801 lane_count: 1,
802 model_id,
803 symbol_count: decoded_length,
804 decoded_length,
805 initial_state: 1,
806 payload: vec![0u8; 4],
807 }
808 }
809
810 #[test]
811 fn model_and_channel_roundtrip() {
812 let payload = b"channel bytes";
813 let mut d = sample(payload);
814 d.models = vec![EntropyModel::uniform(8).unwrap()];
815 d.channels = vec![channel(0, 8, payload.len() as u64)];
816 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
817 let (bytes, cost) = d.serialize().unwrap();
818 assert_eq!(cost.total(), bytes.len() as u64);
819 assert!(cost.models > 0);
820 assert!(cost.entropy_payload > 0);
821 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
822 assert_eq!(parsed.descriptor, d);
823 assert_eq!(parsed.cost.total(), bytes.len() as u64);
824 }
825
826 #[test]
827 fn channel_with_missing_model_rejected() {
828 let mut d = sample(b"abc");
829 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
830 d.channels = vec![channel(3, 8, 3)];
831 let (bytes, _) = d.serialize().unwrap();
832 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
833 assert_eq!(e.class(), crate::ErrorClass::InvalidModel);
834 }
835
836 #[test]
837 fn channel_scale_mismatch_rejected() {
838 let mut d = sample(b"abc");
839 d.models = vec![EntropyModel::uniform(8).unwrap()];
840 d.program = Program::new(vec![Op::DecodeChannel { channel_id: 0 }]);
841 d.channels = vec![channel(0, 12, 3)];
842 let (bytes, _) = d.serialize().unwrap();
843 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
844 assert_eq!(e.class(), crate::ErrorClass::InvalidModel);
845 }
846
847 #[test]
848 fn serialize_parse_roundtrip() {
849 let d = sample(b"hello, exact world");
850 let (bytes, cost) = d.serialize().unwrap();
851 assert_eq!(cost.total(), bytes.len() as u64);
852 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
853 assert_eq!(parsed.descriptor, d);
854 assert_eq!(parsed.cost.total(), bytes.len() as u64);
855 }
856
857 #[test]
858 fn trailing_bytes_after_trailer_rejected() {
859 let d = sample(b"abc");
860 let (mut bytes, _) = d.serialize().unwrap();
861 bytes.push(0);
862 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
863 assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
864 }
865
866 #[test]
867 fn declared_length_mismatch_rejected() {
868 let mut d = sample(b"abcdef");
870 d.source_len = 5;
871 let (bytes, _) = d.serialize().unwrap();
872 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
873 assert_eq!(e.class(), crate::ErrorClass::CoverageViolation);
874 }
875
876 fn replay_program() -> Program {
877 Program::new(vec![Op::DeflateReplay {
878 replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
879 source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
880 source_id: 0,
881 corrections_object: 0,
882 declared_output_len: 3,
883 }])
884 }
885
886 #[test]
887 fn plain_descriptor_declares_no_mandatory_features() {
888 assert_eq!(sample(b"abc").required_features(), 0);
889 }
890
891 #[cfg(feature = "deflate-replay")]
892 #[test]
893 fn replay_op_declares_mandatory_feature() {
894 let mut d = sample(b"abc");
895 d.program = replay_program();
896 assert_eq!(
897 d.required_features(),
898 crate::container::header::FEATURE_DEFLATE_REPLAY
899 );
900 let (bytes, _) = d.serialize().unwrap();
902 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
903 assert_eq!(
904 parsed.descriptor.required_features(),
905 crate::container::header::FEATURE_DEFLATE_REPLAY
906 );
907 }
908
909 #[cfg(not(feature = "deflate-replay"))]
910 #[test]
911 fn replay_descriptor_fails_closed_without_feature() {
912 let mut d = sample(b"abc");
913 d.program = replay_program();
914 let (bytes, _) = d.serialize().unwrap();
915 let e = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap_err();
916 assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
917 }
918
919 use crate::container::observation::{
920 DEP_NONE, DEP_OBJECT, ObservationDigest, ObservationSelector, OpEntry, SECTION_DIGESTS,
921 SECTION_OP_TABLE, SECTION_PDF_SELECTORS, SELECTOR_OBJECT,
922 };
923
924 fn indexed_descriptor() -> Descriptor {
927 let mut d = sample(b"");
928 d.objects = vec![ObjectSource::Inline(b"abc".to_vec())];
929 d.program = Program::new(vec![
930 Op::EmitObject { object_id: 0 },
931 Op::Inline {
932 bytes: b"de".to_vec(),
933 },
934 ]);
935 d.source_sha256 = sha256(b"abcde");
936 d.source_len = 5;
937 d.observation_index = Some(ObservationIndex {
938 section_flags: SECTION_OP_TABLE | SECTION_PDF_SELECTORS | SECTION_DIGESTS,
939 ops: vec![
940 OpEntry {
941 out_len: 3,
942 dep_kind: DEP_OBJECT,
943 dep_id: 0,
944 },
945 OpEntry {
946 out_len: 2,
947 dep_kind: DEP_NONE,
948 dep_id: 0,
949 },
950 ],
951 selectors: vec![ObservationSelector {
952 kind: SELECTOR_OBJECT,
953 number: 1,
954 generation: 0,
955 out_off: 0,
956 out_len: 3,
957 }],
958 digests: vec![ObservationDigest {
959 out_off: 3,
960 out_len: 2,
961 sha256: [7u8; 32],
962 }],
963 });
964 d
965 }
966
967 #[test]
968 fn observation_index_roundtrip_and_charge() {
969 let d = indexed_descriptor();
970 assert_eq!(
971 d.optional_features(),
972 crate::container::header::FEATURE_OBSERVATION_INDEX
973 );
974 let (bytes, cost) = d.serialize().unwrap();
975 assert_eq!(
976 cost.total(),
977 bytes.len() as u64,
978 "cost must be the byte length"
979 );
980 assert!(
981 cost.index > 0,
982 "the index payload + framing must be charged"
983 );
984
985 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
986 assert_eq!(parsed.descriptor, d);
987 assert_eq!(parsed.cost.total(), bytes.len() as u64);
988 assert!(parsed.cost.index > 0);
989
990 let plain = sample(b"nope");
992 let (pbytes, pcost) = plain.serialize().unwrap();
993 assert_eq!(pcost.total(), pbytes.len() as u64);
994 assert_eq!(pcost.index, 0);
995 assert_eq!(plain.optional_features(), 0);
996 let reparsed = Descriptor::parse(&pbytes, Limits::DEFAULT).unwrap();
997 assert!(reparsed.descriptor.observation_index.is_none());
998 }
999
1000 #[test]
1001 fn inconsistent_observation_index_is_rejected_on_parse() {
1002 let mut d = indexed_descriptor();
1004 d.observation_index.as_mut().unwrap().ops[0].out_len = 9;
1005 let (bytes, _) = d.serialize().unwrap();
1006 assert_eq!(
1007 Descriptor::parse(&bytes, Limits::DEFAULT)
1008 .unwrap_err()
1009 .class(),
1010 crate::ErrorClass::CoverageViolation
1011 );
1012
1013 let mut d = indexed_descriptor();
1014 let idx = d.observation_index.as_mut().unwrap();
1015 idx.ops[0].dep_kind = DEP_OBJECT;
1016 idx.ops[0].dep_id = 99;
1017 let (bytes, _) = d.serialize().unwrap();
1018 assert_eq!(
1019 Descriptor::parse(&bytes, Limits::DEFAULT)
1020 .unwrap_err()
1021 .class(),
1022 crate::ErrorClass::CoverageViolation
1023 );
1024
1025 let mut d = indexed_descriptor();
1026 let sel = &mut d.observation_index.as_mut().unwrap().selectors[0];
1027 sel.out_off = 4;
1028 sel.out_len = 9;
1029 let (bytes, _) = d.serialize().unwrap();
1030 assert_eq!(
1031 Descriptor::parse(&bytes, Limits::DEFAULT)
1032 .unwrap_err()
1033 .class(),
1034 crate::ErrorClass::CoverageViolation
1035 );
1036 }
1037
1038 fn seekable_descriptor() -> Descriptor {
1045 let mut d = indexed_descriptor();
1046 d.seek_directory = true;
1047 d
1048 }
1049
1050 fn rebuild_with_directory(bytes: &[u8], mut mutate: impl FnMut(&mut SeekDirectory)) -> Vec<u8> {
1055 use crate::container::record::{RecordReader, write_record};
1056 let header = &bytes[0..HEADER_LEN];
1057 let mut reader = RecordReader::new(bytes, HEADER_LEN, Limits::DEFAULT);
1058 let mut records = Vec::new();
1059 while let Some(r) = reader.next_record().unwrap() {
1060 records.push(r);
1061 }
1062 let mut out = header.to_vec();
1063 for r in &records {
1064 if r.tag == RecordTag::Directory as u8 {
1065 let mut dir = SeekDirectory::decode(&r.payload, Limits::DEFAULT).unwrap();
1066 mutate(&mut dir);
1067 write_record(&mut out, r.tag, r.flags, &dir.encode().unwrap()).unwrap();
1068 } else {
1069 write_record(&mut out, r.tag, r.flags, &r.payload).unwrap();
1070 }
1071 }
1072 out
1073 }
1074
1075 #[test]
1076 fn seek_directory_roundtrips_materializes_and_charges() {
1077 let d = seekable_descriptor();
1078 assert_eq!(
1079 d.optional_features(),
1080 crate::container::header::FEATURE_OBSERVATION_INDEX
1081 | crate::container::header::FEATURE_SEEK_DIRECTORY
1082 );
1083 let (bytes, cost) = d.serialize().unwrap();
1084 assert_eq!(cost.total(), bytes.len() as u64, "cost must be the length");
1085 assert!(
1086 cost.directory > 0,
1087 "the directory payload + framing is charged"
1088 );
1089
1090 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1091 assert_eq!(parsed.descriptor, d, "seekable descriptor must round-trip");
1092 assert!(parsed.descriptor.seek_directory);
1093 assert_eq!(parsed.cost.total(), bytes.len() as u64);
1094 assert_eq!(parsed.cost.directory, cost.directory);
1095
1096 let out = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT)
1098 .unwrap()
1099 .0;
1100 assert_eq!(out, b"abcde");
1101 }
1102
1103 #[test]
1104 fn directory_is_the_first_record_and_is_optional() {
1105 let (bytes, _) = seekable_descriptor().serialize().unwrap();
1106 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1107 let first = r.next_record().unwrap().unwrap();
1108 assert_eq!(first.tag, RecordTag::Directory as u8);
1109 assert!(
1110 first.is_optional(),
1111 "the directory must carry FLAG_OPTIONAL"
1112 );
1113 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1115 r.next_record().unwrap().unwrap();
1116 assert_eq!(
1117 r.position(),
1118 HEADER_LEN + RECORD_OVERHEAD + first.payload.len(),
1119 "the next record must begin right after the directory"
1120 );
1121 }
1122
1123 #[test]
1124 fn non_seekable_descriptor_has_no_directory_cost() {
1125 let d = sample(b"no directory here");
1126 let (bytes, cost) = d.serialize().unwrap();
1127 assert_eq!(cost.directory, 0);
1128 assert_eq!(cost.total(), bytes.len() as u64);
1129 assert_eq!(d.optional_features(), 0);
1130 let mut r = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1133 assert_eq!(
1134 r.next_record().unwrap().unwrap().tag,
1135 RecordTag::Universe as u8
1136 );
1137 let parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1138 assert!(!parsed.descriptor.seek_directory);
1139 assert_eq!(parsed.cost.directory, 0);
1140 }
1141
1142 #[test]
1143 fn seek_directory_without_index_is_rejected() {
1144 let mut d = sample(b"abc");
1145 d.seek_directory = true;
1146 assert_eq!(
1147 d.serialize().unwrap_err().class(),
1148 crate::ErrorClass::InvalidContainer
1149 );
1150 }
1151
1152 #[test]
1153 fn corrupted_directory_payload_is_rejected() {
1154 let (mut bytes, _) = seekable_descriptor().serialize().unwrap();
1155 bytes[HEADER_LEN + 8] ^= 0x01;
1158 assert_eq!(
1159 Descriptor::parse(&bytes, Limits::DEFAULT)
1160 .unwrap_err()
1161 .class(),
1162 crate::ErrorClass::InvalidContainer
1163 );
1164 }
1165
1166 #[test]
1167 fn lying_directory_is_rejected_on_parse() {
1168 let (bytes, _) = seekable_descriptor().serialize().unwrap();
1169 Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
1171
1172 let lying = rebuild_with_directory(&bytes, |dir| dir.entries[1].offset += 1);
1174 assert_eq!(
1175 Descriptor::parse(&lying, Limits::DEFAULT)
1176 .unwrap_err()
1177 .class(),
1178 crate::ErrorClass::InvalidContainer
1179 );
1180
1181 let lying = rebuild_with_directory(&bytes, |dir| dir.classes[0].count += 1);
1183 assert_eq!(
1184 Descriptor::parse(&lying, Limits::DEFAULT)
1185 .unwrap_err()
1186 .class(),
1187 crate::ErrorClass::InvalidContainer
1188 );
1189
1190 let mut reader = RecordReader::new(&bytes, HEADER_LEN, Limits::DEFAULT);
1193 let mut records = Vec::new();
1194 while let Some(r) = reader.next_record().unwrap() {
1195 records.push(r);
1196 }
1197 let mut out = bytes[0..HEADER_LEN].to_vec();
1198 for r in &records {
1199 let flags = if r.tag == RecordTag::Directory as u8 {
1200 0
1201 } else {
1202 r.flags
1203 };
1204 crate::container::record::write_record(&mut out, r.tag, flags, &r.payload).unwrap();
1205 }
1206 assert_eq!(
1207 Descriptor::parse(&out, Limits::DEFAULT)
1208 .unwrap_err()
1209 .class(),
1210 crate::ErrorClass::InvalidContainer
1211 );
1212 }
1213}