1use std::collections::BTreeMap;
5use std::num::NonZeroU32;
6
7use crate::decode::u64_from_index;
8#[cfg(feature = "schema")]
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12macro_rules! label_enum {
13 ($(#[$meta:meta])* $name:ident { $($variant:ident => $label:literal,)* }) => {
14 $(#[$meta])*
15 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16 #[cfg_attr(feature = "schema", derive(JsonSchema))]
17 pub enum $name {
18 $(
19 #[doc = concat!("The `", $label, "` classification.")]
20 #[serde(rename = $label)]
21 $variant,
22 )*
23 }
24
25 impl $name {
26 #[must_use]
28 pub const fn as_str(self) -> &'static str {
29 match self { $(Self::$variant => $label,)* }
30 }
31 }
32
33 impl std::fmt::Display for $name {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.write_str(self.as_str())
36 }
37 }
38 };
39}
40
41label_enum! {
42 ContainerRole {
44 AcisBinary => "acis-binary",
45 ActiveBodyIndex => "active-body-index",
46 Ancillary => "ancillary",
47 Arrangements => "arrangements",
48 AssetCatalog => "asset-catalog",
49 Auxiliary => "auxiliary",
50 Block => "block",
51 Brep => "brep",
52 BrepSmb => "brep-smb",
53 BrepSmbh => "brep-smbh",
54 BrepText => "brep-text",
55 Bulkstream => "bulkstream",
56 CacheCell => "cache-cell",
57 CompoundStream => "compound-stream",
58 DesignConfig => "design-config",
59 Directory => "directory",
60 DirectoryEntry => "directory-entry",
61 DisplayJt => "display-jt",
62 Document => "document",
63 EntityRecords => "entity-records",
64 ExternalReference => "external-reference",
65 ExternalReferences => "external-references",
66 FastLoadJt => "fast-load-jt",
67 FastLoadStructure => "fast-load-structure",
68 FinjplSegment => "finjpl-segment",
69 GuiDocument => "gui-document",
70 Image => "image",
71 InFileAnchors => "in-file-anchors",
72 Manifest => "manifest",
73 MaterialTexture => "material-texture",
74 Metadata => "metadata",
75 Metastream => "metastream",
76 ModelData => "model-data",
77 NamedOpaqueStream => "named-opaque-stream",
78 NestedArchive => "nested-archive",
79 ObjectClass => "object-class",
80 OgsCache => "ogs-cache",
81 Opaque => "opaque",
82 Other => "other",
83 Paramesh => "paramesh",
84 ParasolidStream => "parasolid-stream",
85 PartAttributes => "part-attributes",
86 PartPayload => "part-payload",
87 Preview => "preview",
88 PreviewImage => "preview-image",
89 Properties => "properties",
90 Protein => "protein",
91 ProteinAssets => "protein-assets",
92 PsbGeometry => "psb-geometry",
93 RetainedTrailingRecords => "retained-trailing-records",
94 RootExchange => "root-exchange",
95 RseDatabase => "rse-database",
96 RseRevisionTable => "rse-revision-table",
97 RseSegmentBulk => "rse-segment-bulk",
98 RseSegmentMetadata => "rse-segment-metadata",
99 RseSegmentRegistry => "rse-segment-registry",
100 RseStorage => "rse-storage",
101 SaveToggleInfo => "save-toggle-info",
102 Section => "section",
103 Signature => "signature",
104 Storage => "storage",
105 Stream => "stream",
106 SubsidiaryExchange => "subsidiary-exchange",
107 Table => "table",
108 Thumbnail => "thumbnail",
109 }
110}
111
112label_enum! {
113 VerbatimLabel {
115 None => "none",
116 Stored => "stored",
117 }
118}
119
120label_enum! {
121 CompressionMethod {
123 Deflate => "deflate",
124 Zstd => "zstd",
125 Jpeg => "jpeg",
126 UnixCompress => "unix-compress",
127 Zlib => "zlib",
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum VerbatimSize {
138 Unreported,
140 PayloadOnly(u64),
142 StoredOnly(u64),
147 Exact(u64),
149 Framed(FramedSpan),
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(JsonSchema))]
161#[cfg_attr(feature = "schema", schemars(with = "u64"))]
162#[serde(try_from = "u64", into = "u64")]
163pub struct AllocatedLen(usize);
164
165const LENGTH_OVER_ALLOCATION: &str = "declared length exceeds the largest live allocation";
167
168impl AllocatedLen {
169 #[must_use]
171 pub fn new(bytes: u64) -> Option<Self> {
172 if bytes > u64_from_index(isize::MAX.unsigned_abs()) {
173 return None;
174 }
175 Some(Self(usize::try_from(bytes).ok()?))
176 }
177
178 #[must_use]
180 pub fn get(self) -> u64 {
181 u64_from_index(self.0)
182 }
183}
184
185impl TryFrom<u64> for AllocatedLen {
186 type Error = &'static str;
187
188 fn try_from(bytes: u64) -> Result<Self, Self::Error> {
189 Self::new(bytes).ok_or(LENGTH_OVER_ALLOCATION)
190 }
191}
192
193impl From<AllocatedLen> for u64 {
194 fn from(length: AllocatedLen) -> Self {
195 length.get()
196 }
197}
198
199impl From<&[u8]> for AllocatedLen {
200 fn from(bytes: &[u8]) -> Self {
201 Self(bytes.len())
202 }
203}
204
205impl From<&str> for AllocatedLen {
206 fn from(text: &str) -> Self {
207 Self(text.len())
208 }
209}
210
211const _: () = assert!(usize::BITS <= 64);
214const _: () = assert!(isize::BITS <= 64);
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct FramedSpan {
227 payload: AllocatedLen,
228 framing: NonZeroU32,
229}
230
231impl FramedSpan {
232 #[must_use]
239 pub const fn from_parts(payload: AllocatedLen, framing: NonZeroU32) -> Self {
240 Self { payload, framing }
241 }
242
243 #[must_use]
245 pub fn payload(self) -> u64 {
246 self.payload.get()
247 }
248
249 #[must_use]
256 pub fn stored(self) -> u64 {
257 self.payload.get() + u64::from(self.framing.get())
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum EntryStorage {
264 Directory,
266 Verbatim {
268 label: VerbatimLabel,
270 size: VerbatimSize,
272 },
273 Compressed {
280 method: CompressionMethod,
282 stored: Option<u64>,
284 expanded: Option<u64>,
286 },
287}
288
289impl EntryStorage {
290 #[must_use]
292 pub const fn verbatim(label: VerbatimLabel, size: u64) -> Self {
293 Self::Verbatim {
294 label,
295 size: VerbatimSize::Exact(size),
296 }
297 }
298
299 #[must_use]
301 pub const fn payload_only(label: VerbatimLabel, payload: u64) -> Self {
302 Self::Verbatim {
303 label,
304 size: VerbatimSize::PayloadOnly(payload),
305 }
306 }
307
308 pub fn framed(
315 label: VerbatimLabel,
316 payload: u64,
317 stored_span: u64,
318 ) -> Result<Self, &'static str> {
319 let Some(framing) = stored_span.checked_sub(payload) else {
320 return Err("verbatim container entry stores fewer bytes than it expands to");
321 };
322 if framing == 0 {
323 return Ok(Self::Verbatim {
324 label,
325 size: VerbatimSize::Exact(stored_span),
326 });
327 }
328 let Some(payload) = AllocatedLen::new(payload) else {
329 return Err(LENGTH_OVER_ALLOCATION);
330 };
331 let Some(framing) = u32::try_from(framing).ok().and_then(NonZeroU32::new) else {
332 return Err("verbatim container entry declares more framing than a span can carry");
333 };
334 Ok(Self::Verbatim {
335 label,
336 size: VerbatimSize::Framed(FramedSpan::from_parts(payload, framing)),
337 })
338 }
339
340 #[must_use]
346 pub fn framed_by(label: VerbatimLabel, payload: AllocatedLen, framing: NonZeroU32) -> Self {
347 Self::Verbatim {
348 label,
349 size: VerbatimSize::Framed(FramedSpan::from_parts(payload, framing)),
350 }
351 }
352
353 #[must_use]
355 pub const fn unreported(label: VerbatimLabel) -> Self {
356 Self::Verbatim {
357 label,
358 size: VerbatimSize::Unreported,
359 }
360 }
361
362 #[must_use]
364 pub fn stored_size(&self) -> Option<u64> {
365 match self {
366 Self::Directory => None,
367 Self::Verbatim { size, .. } => match size {
368 VerbatimSize::Unreported | VerbatimSize::PayloadOnly(_) => None,
369 VerbatimSize::StoredOnly(size) | VerbatimSize::Exact(size) => Some(*size),
370 VerbatimSize::Framed(span) => Some(span.stored()),
371 },
372 Self::Compressed { stored, .. } => *stored,
373 }
374 }
375
376 #[must_use]
378 pub fn expanded_size(&self) -> Option<u64> {
379 match self {
380 Self::Directory => None,
381 Self::Verbatim { size, .. } => match size {
382 VerbatimSize::Unreported | VerbatimSize::StoredOnly(_) => None,
383 VerbatimSize::PayloadOnly(payload) | VerbatimSize::Exact(payload) => Some(*payload),
384 VerbatimSize::Framed(span) => Some(span.payload()),
385 },
386 Self::Compressed { expanded, .. } => *expanded,
387 }
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396#[cfg_attr(feature = "schema", derive(JsonSchema))]
397#[cfg_attr(feature = "schema", schemars(with = "ContainerEntryWire"))]
398#[serde(from = "ContainerEntryWire", into = "ContainerEntryWire")]
399pub struct ContainerEntry {
400 pub name: String,
402 pub role: ContainerRole,
404 pub storage: EntryStorage,
406 #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
408 pub attributes: BTreeMap<String, String>,
409}
410
411impl ContainerEntry {
412 #[must_use]
414 pub fn stored_size(&self) -> Option<u64> {
415 self.storage.stored_size()
416 }
417
418 #[must_use]
420 pub fn expanded_size(&self) -> Option<u64> {
421 self.storage.expanded_size()
422 }
423}
424
425#[derive(Serialize, Deserialize)]
427#[cfg_attr(feature = "schema", derive(JsonSchema))]
428#[serde(deny_unknown_fields)]
429struct EntryIdentityWire {
430 name: String,
432 role: ContainerRole,
434 #[serde(default)]
436 #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
437 attributes: BTreeMap<String, String>,
438}
439
440#[derive(Clone, Copy, Serialize, Deserialize)]
445#[cfg_attr(feature = "schema", derive(JsonSchema))]
446#[serde(deny_unknown_fields)]
447struct CompressedSizesWire {
448 #[serde(
450 default,
451 skip_serializing_if = "Option::is_none",
452 deserialize_with = "deserialize_stored"
453 )]
454 stored: Option<u64>,
455 #[serde(
457 default,
458 skip_serializing_if = "Option::is_none",
459 deserialize_with = "deserialize_expanded"
460 )]
461 expanded: Option<u64>,
462}
463
464#[derive(Clone, Copy, Serialize, Deserialize)]
470#[cfg_attr(feature = "schema", derive(JsonSchema))]
471#[serde(tag = "form", rename_all = "snake_case")]
472#[serde(deny_unknown_fields)]
473enum VerbatimSizeWire {
474 Unreported {},
476 PayloadOnly {
478 payload: u64,
480 },
481 StoredOnly {
483 stored: u64,
485 },
486 Exact {
488 size: u64,
490 },
491 Framed {
493 payload: AllocatedLen,
495 framing: NonZeroU32,
497 },
498}
499
500impl From<VerbatimSize> for VerbatimSizeWire {
501 fn from(size: VerbatimSize) -> Self {
502 match size {
503 VerbatimSize::Unreported => Self::Unreported {},
504 VerbatimSize::PayloadOnly(payload) => Self::PayloadOnly { payload },
505 VerbatimSize::StoredOnly(stored) => Self::StoredOnly { stored },
506 VerbatimSize::Exact(size) => Self::Exact { size },
507 VerbatimSize::Framed(span) => Self::Framed {
508 payload: span.payload,
509 framing: span.framing,
510 },
511 }
512 }
513}
514
515impl From<VerbatimSizeWire> for VerbatimSize {
516 fn from(wire: VerbatimSizeWire) -> Self {
517 match wire {
518 VerbatimSizeWire::Unreported {} => Self::Unreported,
519 VerbatimSizeWire::PayloadOnly { payload } => Self::PayloadOnly(payload),
520 VerbatimSizeWire::StoredOnly { stored } => Self::StoredOnly(stored),
521 VerbatimSizeWire::Exact { size } => Self::Exact(size),
522 VerbatimSizeWire::Framed { payload, framing } => {
523 Self::Framed(FramedSpan::from_parts(payload, framing))
524 }
525 }
526 }
527}
528
529#[derive(Serialize, Deserialize)]
535#[cfg_attr(feature = "schema", derive(JsonSchema))]
536#[serde(tag = "compression")]
537#[serde(deny_unknown_fields)]
538enum ContainerEntryWire {
539 #[serde(rename = "storage")]
541 Directory {
542 #[serde(flatten)]
544 identity: EntryIdentityWire,
545 },
546 #[serde(rename = "none")]
548 VerbatimNone {
549 #[serde(flatten)]
551 identity: EntryIdentityWire,
552 size: VerbatimSizeWire,
554 },
555 #[serde(rename = "stored")]
557 VerbatimStored {
558 #[serde(flatten)]
560 identity: EntryIdentityWire,
561 size: VerbatimSizeWire,
563 },
564 #[serde(rename = "deflate")]
566 Deflate {
567 #[serde(flatten)]
569 identity: EntryIdentityWire,
570 #[serde(flatten)]
572 sizes: CompressedSizesWire,
573 },
574 #[serde(rename = "zstd")]
576 Zstd {
577 #[serde(flatten)]
579 identity: EntryIdentityWire,
580 #[serde(flatten)]
582 sizes: CompressedSizesWire,
583 },
584 #[serde(rename = "jpeg")]
586 Jpeg {
587 #[serde(flatten)]
589 identity: EntryIdentityWire,
590 #[serde(flatten)]
592 sizes: CompressedSizesWire,
593 },
594 #[serde(rename = "unix-compress")]
596 UnixCompress {
597 #[serde(flatten)]
599 identity: EntryIdentityWire,
600 #[serde(flatten)]
602 sizes: CompressedSizesWire,
603 },
604 #[serde(rename = "zlib")]
606 Zlib {
607 #[serde(flatten)]
609 identity: EntryIdentityWire,
610 #[serde(flatten)]
612 sizes: CompressedSizesWire,
613 },
614}
615
616impl From<ContainerEntry> for ContainerEntryWire {
617 fn from(entry: ContainerEntry) -> Self {
618 let identity = EntryIdentityWire {
619 name: entry.name,
620 role: entry.role,
621 attributes: entry.attributes,
622 };
623 match entry.storage {
624 EntryStorage::Directory => Self::Directory { identity },
625 EntryStorage::Verbatim { label, size } => {
626 let size = size.into();
627 match label {
628 VerbatimLabel::None => Self::VerbatimNone { identity, size },
629 VerbatimLabel::Stored => Self::VerbatimStored { identity, size },
630 }
631 }
632 EntryStorage::Compressed {
633 method,
634 stored,
635 expanded,
636 } => {
637 let sizes = CompressedSizesWire { stored, expanded };
638 match method {
639 CompressionMethod::Deflate => Self::Deflate { identity, sizes },
640 CompressionMethod::Zstd => Self::Zstd { identity, sizes },
641 CompressionMethod::Jpeg => Self::Jpeg { identity, sizes },
642 CompressionMethod::UnixCompress => Self::UnixCompress { identity, sizes },
643 CompressionMethod::Zlib => Self::Zlib { identity, sizes },
644 }
645 }
646 }
647 }
648}
649
650impl From<ContainerEntryWire> for ContainerEntry {
651 fn from(wire: ContainerEntryWire) -> Self {
652 let verbatim = |label: VerbatimLabel, size: VerbatimSizeWire| EntryStorage::Verbatim {
653 label,
654 size: size.into(),
655 };
656 let compressed =
657 |method: CompressionMethod, sizes: CompressedSizesWire| EntryStorage::Compressed {
658 method,
659 stored: sizes.stored,
660 expanded: sizes.expanded,
661 };
662 let (identity, storage) = match wire {
663 ContainerEntryWire::Directory { identity } => (identity, EntryStorage::Directory),
664 ContainerEntryWire::VerbatimNone { identity, size } => {
665 (identity, verbatim(VerbatimLabel::None, size))
666 }
667 ContainerEntryWire::VerbatimStored { identity, size } => {
668 (identity, verbatim(VerbatimLabel::Stored, size))
669 }
670 ContainerEntryWire::Deflate { identity, sizes } => {
671 (identity, compressed(CompressionMethod::Deflate, sizes))
672 }
673 ContainerEntryWire::Zstd { identity, sizes } => {
674 (identity, compressed(CompressionMethod::Zstd, sizes))
675 }
676 ContainerEntryWire::Jpeg { identity, sizes } => {
677 (identity, compressed(CompressionMethod::Jpeg, sizes))
678 }
679 ContainerEntryWire::UnixCompress { identity, sizes } => {
680 (identity, compressed(CompressionMethod::UnixCompress, sizes))
681 }
682 ContainerEntryWire::Zlib { identity, sizes } => {
683 (identity, compressed(CompressionMethod::Zlib, sizes))
684 }
685 };
686 Self {
687 name: identity.name,
688 role: identity.role,
689 storage,
690 attributes: identity.attributes,
691 }
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::u64_from_index;
698 use super::{
699 CompressionMethod, ContainerEntry, ContainerRole, EntryStorage, VerbatimLabel, VerbatimSize,
700 };
701 use std::collections::BTreeMap;
702 use std::num::{NonZeroU32, NonZeroU64};
703
704 fn verbatim_wire(compression: &str, size: &serde_json::Value) -> serde_json::Value {
705 serde_json::json!({
706 "name": "entry",
707 "role": "stream",
708 "compression": compression,
709 "size": size.clone(),
710 })
711 }
712
713 fn compressed_wire(
714 compression: &str,
715 stored: Option<u64>,
716 expanded: Option<u64>,
717 ) -> serde_json::Value {
718 let mut value = serde_json::json!({
719 "name": "entry",
720 "role": "stream",
721 "compression": compression,
722 });
723 if let Some(stored) = stored {
724 value["stored"] = stored.into();
725 }
726 if let Some(expanded) = expanded {
727 value["expanded"] = expanded.into();
728 }
729 value
730 }
731
732 fn admit(wire: serde_json::Value) -> ContainerEntry {
733 serde_json::from_value(wire).expect("the declared sizes are admissible")
734 }
735
736 fn reject(wire: serde_json::Value) -> String {
737 serde_json::from_value::<ContainerEntry>(wire)
738 .expect_err("the declared sizes are inadmissible")
739 .to_string()
740 }
741
742 fn nonzero(value: u64) -> NonZeroU64 {
743 NonZeroU64::new(value).expect("a nonzero test size")
744 }
745
746 fn entry(storage: EntryStorage) -> ContainerEntry {
747 ContainerEntry {
748 name: "entry".to_string(),
749 role: ContainerRole::Stream,
750 storage,
751 attributes: BTreeMap::new(),
752 }
753 }
754
755 fn framed(payload: u64, framing: u32) -> VerbatimSize {
756 VerbatimSize::Framed(super::FramedSpan::from_parts(
757 super::AllocatedLen::new(payload).expect("a live payload length"),
758 NonZeroU32::new(framing).expect("a nonzero test framing"),
759 ))
760 }
761
762 #[test]
763 fn each_declared_verbatim_size_has_one_spelling() {
764 for (size, expected) in [
765 (
766 serde_json::json!({"form": "unreported"}),
767 VerbatimSize::Unreported,
768 ),
769 (
770 serde_json::json!({"form": "payload_only", "payload": 12}),
771 VerbatimSize::PayloadOnly(12),
772 ),
773 (
774 serde_json::json!({"form": "payload_only", "payload": 0}),
775 VerbatimSize::PayloadOnly(0),
776 ),
777 (
778 serde_json::json!({"form": "stored_only", "stored": 5}),
779 VerbatimSize::StoredOnly(5),
780 ),
781 (
782 serde_json::json!({"form": "exact", "size": 7}),
783 VerbatimSize::Exact(7),
784 ),
785 (
786 serde_json::json!({"form": "exact", "size": 0}),
787 VerbatimSize::Exact(0),
788 ),
789 (
790 serde_json::json!({"form": "framed", "payload": 10, "framing": 20}),
791 framed(10, 20),
792 ),
793 (
794 serde_json::json!({"form": "framed", "payload": 0, "framing": 5}),
795 framed(0, 5),
796 ),
797 ] {
798 let storage = EntryStorage::Verbatim {
799 label: VerbatimLabel::None,
800 size: expected,
801 };
802 let read = admit(verbatim_wire("none", &size));
803 assert_eq!(read.storage, storage, "{size}");
804 let mut expected_wire = verbatim_wire("none", &size);
805 expected_wire["attributes"] = serde_json::json!({});
806 assert_eq!(
807 serde_json::to_value(entry(storage)).expect("a container entry serializes"),
808 expected_wire,
809 "{size}"
810 );
811 }
812 }
813
814 #[test]
815 fn a_framed_payload_carries_no_stored_span_to_disagree_with() {
816 let error = reject(verbatim_wire(
817 "none",
818 &serde_json::json!({"form": "framed", "payload": 9, "framing": 1, "stored": 5}),
819 ));
820 assert!(error.contains("unknown field"), "{error}");
821 assert!(error.contains("stored"), "{error}");
822
823 let error = reject(verbatim_wire(
824 "none",
825 &serde_json::json!({"form": "framed", "payload": 9, "framing": 0}),
826 ));
827 assert!(error.contains("zero"), "{error}");
828
829 let error = reject(verbatim_wire(
830 "none",
831 &serde_json::json!({"form": "framed", "payload": u64::MAX, "framing": 1}),
832 ));
833 assert!(
834 error.contains("declared length exceeds the largest live allocation"),
835 "{error}"
836 );
837 }
838
839 #[test]
840 fn a_container_door_refuses_a_span_under_its_payload() {
841 assert_eq!(
842 EntryStorage::framed(VerbatimLabel::Stored, 9, 5),
843 Err("verbatim container entry stores fewer bytes than it expands to")
844 );
845 assert_eq!(
846 EntryStorage::framed(VerbatimLabel::Stored, 9, 9),
847 Ok(EntryStorage::verbatim(VerbatimLabel::Stored, 9))
848 );
849 assert_eq!(
850 EntryStorage::framed(VerbatimLabel::Stored, 10, 30),
851 Ok(EntryStorage::Verbatim {
852 label: VerbatimLabel::Stored,
853 size: framed(10, 20),
854 })
855 );
856 assert_eq!(
857 EntryStorage::framed(VerbatimLabel::Stored, 0, u64::from(u32::MAX) + 1),
858 Err("verbatim container entry declares more framing than a span can carry")
859 );
860 }
861
862 #[test]
863 fn compressed_sizes_spell_unreported_only_as_absence() {
864 for (stored, expanded) in [
865 (None, None),
866 (None, Some(99)),
867 (Some(44), None),
868 (Some(4), Some(16)),
869 (Some(0), Some(0)),
870 ] {
871 let storage = EntryStorage::Compressed {
872 method: CompressionMethod::Deflate,
873 stored,
874 expanded,
875 };
876 let mut expected = compressed_wire("deflate", stored, expanded);
877 expected["attributes"] = serde_json::json!({});
878 let serialized =
879 serde_json::to_value(entry(storage.clone())).expect("a container entry serializes");
880 assert_eq!(serialized, expected);
881 let read_back: ContainerEntry =
882 serde_json::from_value(serialized).expect("the wire value re-admits");
883 assert_eq!(read_back.storage, storage);
884 }
885 }
886
887 #[test]
888 fn a_minted_framed_span_always_exceeds_its_payload() {
889 let empty: &[u8] = &[];
890 let span = super::FramedSpan::from_parts(empty.into(), NonZeroU32::MAX);
891 assert_eq!(span.payload(), 0);
892 assert_eq!(
893 nonzero(span.stored() - span.payload()),
894 nonzero(u64::from(u32::MAX))
895 );
896 assert_eq!(span.stored(), u64::from(u32::MAX));
897 assert!(span.stored() > span.payload());
898 let span = super::FramedSpan::from_parts(empty.into(), NonZeroU32::MIN);
899 assert_eq!(span.stored(), 1);
900 let body = vec![0u8; 12];
901 let span = super::FramedSpan::from_parts(body.as_slice().into(), NonZeroU32::MIN);
902 assert_eq!(span.payload(), u64_from_index(body.len()));
903 assert_eq!(span.stored(), 13);
904 assert_eq!(nonzero(span.stored() - span.payload()), nonzero(1));
905 let span = super::FramedSpan::from_parts("target.CATPart".into(), NonZeroU32::MAX);
906 assert_eq!(span.payload(), u64_from_index("target.CATPart".len()));
907 assert_eq!(
908 span.stored(),
909 u64_from_index("target.CATPart".len()) + u64::from(u32::MAX)
910 );
911 }
912
913 #[test]
914 fn a_reported_zero_size_is_not_an_unreported_one() {
915 let reported = admit(verbatim_wire(
916 "none",
917 &serde_json::json!({"form": "exact", "size": 0}),
918 ));
919 let unreported = admit(verbatim_wire(
920 "none",
921 &serde_json::json!({"form": "unreported"}),
922 ));
923 assert_ne!(reported.storage, unreported.storage);
924 assert_eq!(reported.stored_size(), Some(0));
925 assert_eq!(reported.expanded_size(), Some(0));
926 assert_eq!(unreported.stored_size(), None);
927 assert_eq!(unreported.expanded_size(), None);
928 assert_ne!(
929 EntryStorage::verbatim(VerbatimLabel::None, 0),
930 EntryStorage::unreported(VerbatimLabel::None)
931 );
932 }
933
934 #[test]
935 fn a_storage_label_cannot_declare_a_byte_size() {
936 let error = reject(verbatim_wire(
937 "storage",
938 &serde_json::json!({"form": "exact", "size": 4}),
939 ));
940 assert!(error.contains("unknown field"), "{error}");
941 assert!(error.contains("size"), "{error}");
942 let error = reject(compressed_wire("storage", Some(4), Some(7)));
943 assert!(error.contains("unknown field"), "{error}");
944
945 let entry: ContainerEntry = serde_json::from_value(serde_json::json!({
946 "name": "entry",
947 "role": "stream",
948 "compression": "storage",
949 }))
950 .expect("a directory node declares no size");
951 assert_eq!(entry.storage, EntryStorage::Directory);
952 }
953
954 #[test]
955 fn reported_sizes_are_absent_rather_than_zero() {
956 let payload_only = admit(verbatim_wire(
957 "none",
958 &serde_json::json!({"form": "payload_only", "payload": 12}),
959 ));
960 assert_eq!(payload_only.stored_size(), None);
961 assert_eq!(payload_only.expanded_size(), Some(12));
962 let framed_entry = admit(verbatim_wire(
963 "none",
964 &serde_json::json!({"form": "framed", "payload": 10, "framing": 20}),
965 ));
966 assert_eq!(framed_entry.stored_size(), Some(30));
967 assert_eq!(framed_entry.expanded_size(), Some(10));
968 let compressed = admit(compressed_wire("zlib", None, Some(99)));
969 assert_eq!(compressed.stored_size(), None);
970 assert_eq!(compressed.expanded_size(), Some(99));
971 }
972
973 #[test]
974 fn a_container_entry_refuses_an_unknown_key_beside_its_name() {
975 let wire = serde_json::json!({
976 "name": "entry",
977 "role": "stream",
978 "compression": "none",
979 "size": {"form": "unreported"},
980 });
981 serde_json::from_value::<ContainerEntry>(wire.clone()).expect("a legal entry");
982
983 let mut stray = wire;
984 stray["zz_bogus"] = serde_json::json!(1);
985 let error = serde_json::from_value::<ContainerEntry>(stray)
986 .expect_err("an unknown key has no encoding")
987 .to_string();
988 assert!(error.contains("zz_bogus"), "{error}");
989 }
990}
991
992crate::named_optional_field!(deserialize_stored, u64, "stored");
994crate::named_optional_field!(deserialize_expanded, u64, "expanded");