Skip to main content

cadmpeg_core/
container.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Format-independent container entries.
3
4use 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            /// Returns the stable summary label.
27            #[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    /// The closed semantic roles of container summary entries.
43    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    /// How a container spells "these bytes are stored verbatim".
114    VerbatimLabel {
115        None => "none",
116        Stored => "stored",
117    }
118}
119
120label_enum! {
121    /// Compression methods reported by container summaries.
122    CompressionMethod {
123        Deflate => "deflate",
124        Zstd => "zstd",
125        Jpeg => "jpeg",
126        UnixCompress => "unix-compress",
127        Zlib => "zlib",
128    }
129}
130
131/// What a container reports about the size of a verbatim payload.
132///
133/// Every value maps to a distinct declared `(stored, expanded)` pair, and an
134/// unreported size is absent on the wire, so a reported zero is an ordinary
135/// size rather than a second spelling of [`VerbatimSize::Unreported`].
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum VerbatimSize {
138    /// Neither the payload nor its stored span is reported.
139    Unreported,
140    /// The payload size; its stored span is not reported.
141    PayloadOnly(u64),
142    /// The stored span; the payload it expands to is not reported.
143    ///
144    /// The wire pair (`compressed_size` present, `uncompressed_size` absent) is
145    /// its only origin: no codec reports a stored span without its payload.
146    StoredOnly(u64),
147    /// The payload occupies exactly this many stored bytes.
148    Exact(u64),
149    /// The payload, plus the container framing counted in its stored span.
150    Framed(FramedSpan),
151}
152
153/// The byte length of a live allocation.
154///
155/// Rust guarantees no allocation exceeds `isize::MAX` bytes, so a value read
156/// off a slice or a `str` carries that bound with it. A declared length read
157/// from a document is minted against the same bound, so every value of this
158/// type carries it however it arrived.
159#[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
165/// Rejection for a declared length no allocation can reach.
166const LENGTH_OVER_ALLOCATION: &str = "declared length exceeds the largest live allocation";
167
168impl AllocatedLen {
169    /// A declared byte length, absent when it exceeds `isize::MAX`.
170    #[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    /// The length in bytes.
179    #[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
211/// The widening in [`FramedSpan::from_parts`] is exact on this target, so a
212/// payload length reaches the span unchanged.
213const _: () = assert!(usize::BITS <= 64);
214/// A live allocation is at most `isize::MAX` bytes and a framing at most
215/// `u32::MAX`, so their sum is inside `u64`. This is what justifies the
216/// unchecked `+` in [`FramedSpan::stored`], and it justifies nothing else.
217const _: () = assert!(isize::BITS <= 64);
218
219/// A verbatim payload inside a strictly larger stored span.
220///
221/// The framing is the declared number and it is non-zero, so a stored span
222/// that does not exceed its payload is unrepresentable. The stored span is
223/// derived from the pair and never stated beside it, so a span that disagrees
224/// with its own payload and framing is unrepresentable too.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct FramedSpan {
227    payload: AllocatedLen,
228    framing: NonZeroU32,
229}
230
231impl FramedSpan {
232    /// A `payload`-byte payload wrapped in `framing` bytes of container framing.
233    ///
234    /// `payload` is the length of a live allocation and so at most `isize::MAX`
235    /// and `framing` at most `u32::MAX`, and the module assertions above prove
236    /// that sum inside `u64`, so [`Self::stored`] adds it unchecked and this
237    /// mint is total.
238    #[must_use]
239    pub const fn from_parts(payload: AllocatedLen, framing: NonZeroU32) -> Self {
240        Self { payload, framing }
241    }
242
243    /// Payload size in bytes.
244    #[must_use]
245    pub fn payload(self) -> u64 {
246        self.payload.get()
247    }
248
249    /// Stored span in bytes, framing included: at least `payload + 1`, since
250    /// the framing is non-zero.
251    ///
252    /// The payload is a live allocation and the framing a `u32`, which the
253    /// module assertions prove sum inside `u64`, so this addition is total for
254    /// every span the type can hold.
255    #[must_use]
256    pub fn stored(self) -> u64 {
257        self.payload.get() + u64::from(self.framing.get())
258    }
259}
260
261/// How one container summary entry stores its bytes.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum EntryStorage {
264    /// A container directory node that holds no bytes of its own.
265    Directory,
266    /// Bytes stored verbatim, with whatever the container reports about their size.
267    Verbatim {
268        /// How this container spells verbatim storage.
269        label: VerbatimLabel,
270        /// What the container reports about the size.
271        size: VerbatimSize,
272    },
273    /// Bytes stored compressed; either size is absent when the codec does not report it.
274    ///
275    /// The two sizes are independent facts: a compressed stream may occupy
276    /// fewer, the same, or more stored bytes than it expands to (an
277    /// incompressible deflate member grows), so no pair of reported sizes is
278    /// invalid and none is a second spelling of another.
279    Compressed {
280        /// Compression method.
281        method: CompressionMethod,
282        /// Stored size in bytes, absent when the codec does not report it.
283        stored: Option<u64>,
284        /// Expanded size in bytes, absent when the codec does not report it.
285        expanded: Option<u64>,
286    },
287}
288
289impl EntryStorage {
290    /// Verbatim bytes occupying exactly `size` stored bytes.
291    #[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    /// Verbatim bytes whose payload is known and whose stored span is not reported.
300    #[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    /// Verbatim bytes whose stored span may include container framing.
309    ///
310    /// The two numbers are independently declared by the container, so their
311    /// order is a fact about the document: a stored span smaller than the
312    /// payload is a malformed declaration and is rejected here, at the door
313    /// that reads the container, not at the document wire.
314    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    /// Verbatim bytes whose stored span is the payload plus `framing` bytes of
341    /// container framing the producer knows.
342    ///
343    /// Total: [`FramedSpan::from_parts`] mints every `(payload, framing)` pair
344    /// a live allocation and a `u32` framing can state.
345    #[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    /// Verbatim bytes whose size the container does not report.
354    #[must_use]
355    pub const fn unreported(label: VerbatimLabel) -> Self {
356        Self::Verbatim {
357            label,
358            size: VerbatimSize::Unreported,
359        }
360    }
361
362    /// Stored span in bytes, absent when the container reports none.
363    #[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    /// Expanded size in bytes, absent when the container reports none.
377    #[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/// One stream or segment in a container summary.
392///
393/// `role` and `attributes` are codec-defined. The ordered attribute map keeps
394/// the format-independent summary deterministic.
395#[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    /// Entry name/path within the container.
401    pub name: String,
402    /// Codec-defined role classification.
403    pub role: ContainerRole,
404    /// Byte storage of this entry.
405    pub storage: EntryStorage,
406    /// Extra codec-extracted attributes, sorted by key.
407    #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
408    pub attributes: BTreeMap<String, String>,
409}
410
411impl ContainerEntry {
412    /// Stored span in bytes, absent when the entry reports none.
413    #[must_use]
414    pub fn stored_size(&self) -> Option<u64> {
415        self.storage.stored_size()
416    }
417
418    /// Expanded size in bytes, absent when the entry reports none.
419    #[must_use]
420    pub fn expanded_size(&self) -> Option<u64> {
421        self.storage.expanded_size()
422    }
423}
424
425/// The entry fields every storage kind carries.
426#[derive(Serialize, Deserialize)]
427#[cfg_attr(feature = "schema", derive(JsonSchema))]
428#[serde(deny_unknown_fields)]
429struct EntryIdentityWire {
430    /// Entry name/path within the container.
431    name: String,
432    /// Codec-defined role classification.
433    role: ContainerRole,
434    /// Extra codec-extracted attributes, sorted by key.
435    #[serde(default)]
436    #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
437    attributes: BTreeMap<String, String>,
438}
439
440/// What a container reports about a compressed entry's two sizes.
441///
442/// A compressed stream may occupy fewer, the same, or more stored bytes than
443/// it expands to, so the two numbers are independent and either may be absent.
444#[derive(Clone, Copy, Serialize, Deserialize)]
445#[cfg_attr(feature = "schema", derive(JsonSchema))]
446#[serde(deny_unknown_fields)]
447struct CompressedSizesWire {
448    /// Stored size in bytes, absent when the codec does not report it.
449    #[serde(
450        default,
451        skip_serializing_if = "Option::is_none",
452        deserialize_with = "deserialize_stored"
453    )]
454    stored: Option<u64>,
455    /// Expanded size in bytes, absent when the codec does not report it.
456    #[serde(
457        default,
458        skip_serializing_if = "Option::is_none",
459        deserialize_with = "deserialize_expanded"
460    )]
461    expanded: Option<u64>,
462}
463
464/// What a container reports about a verbatim entry's size.
465///
466/// One tagged object names which sizes the container reported. The stored span
467/// of a framed payload is the payload plus the framing, so it is never stated
468/// beside them and cannot disagree with them.
469#[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    /// Neither the payload nor its stored span is reported.
475    Unreported {},
476    /// The payload size alone.
477    PayloadOnly {
478        /// Payload size in bytes.
479        payload: u64,
480    },
481    /// The stored span alone.
482    StoredOnly {
483        /// Stored span in bytes.
484        stored: u64,
485    },
486    /// The payload occupies exactly this many stored bytes.
487    Exact {
488        /// Payload size and stored span in bytes.
489        size: u64,
490    },
491    /// The payload, plus the container framing counted in its stored span.
492    Framed {
493        /// Payload size in bytes.
494        payload: AllocatedLen,
495        /// Framing bytes counted in the stored span but not in the payload.
496        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/// One container summary entry, tagged by how it stores its bytes.
530///
531/// The `storage` spelling is a directory node, which holds no bytes of its
532/// own, so it carries no size keys: a directory that declares a byte size is
533/// unrepresentable rather than refused.
534#[derive(Serialize, Deserialize)]
535#[cfg_attr(feature = "schema", derive(JsonSchema))]
536#[serde(tag = "compression")]
537#[serde(deny_unknown_fields)]
538enum ContainerEntryWire {
539    /// A container directory node that holds no bytes of its own.
540    #[serde(rename = "storage")]
541    Directory {
542        /// The entry fields every storage kind carries.
543        #[serde(flatten)]
544        identity: EntryIdentityWire,
545    },
546    /// Bytes stored verbatim, which this container spells `none`.
547    #[serde(rename = "none")]
548    VerbatimNone {
549        /// The entry fields every storage kind carries.
550        #[serde(flatten)]
551        identity: EntryIdentityWire,
552        /// What the container reports about the size.
553        size: VerbatimSizeWire,
554    },
555    /// Bytes stored verbatim, which this container spells `stored`.
556    #[serde(rename = "stored")]
557    VerbatimStored {
558        /// The entry fields every storage kind carries.
559        #[serde(flatten)]
560        identity: EntryIdentityWire,
561        /// What the container reports about the size.
562        size: VerbatimSizeWire,
563    },
564    /// Bytes stored compressed with deflate.
565    #[serde(rename = "deflate")]
566    Deflate {
567        /// The entry fields every storage kind carries.
568        #[serde(flatten)]
569        identity: EntryIdentityWire,
570        /// What the container reports about the entry's two sizes.
571        #[serde(flatten)]
572        sizes: CompressedSizesWire,
573    },
574    /// Bytes stored compressed with zstd.
575    #[serde(rename = "zstd")]
576    Zstd {
577        /// The entry fields every storage kind carries.
578        #[serde(flatten)]
579        identity: EntryIdentityWire,
580        /// What the container reports about the entry's two sizes.
581        #[serde(flatten)]
582        sizes: CompressedSizesWire,
583    },
584    /// Bytes stored compressed with JPEG.
585    #[serde(rename = "jpeg")]
586    Jpeg {
587        /// The entry fields every storage kind carries.
588        #[serde(flatten)]
589        identity: EntryIdentityWire,
590        /// What the container reports about the entry's two sizes.
591        #[serde(flatten)]
592        sizes: CompressedSizesWire,
593    },
594    /// Bytes stored compressed with Unix compress.
595    #[serde(rename = "unix-compress")]
596    UnixCompress {
597        /// The entry fields every storage kind carries.
598        #[serde(flatten)]
599        identity: EntryIdentityWire,
600        /// What the container reports about the entry's two sizes.
601        #[serde(flatten)]
602        sizes: CompressedSizesWire,
603    },
604    /// Bytes stored compressed with zlib.
605    #[serde(rename = "zlib")]
606    Zlib {
607        /// The entry fields every storage kind carries.
608        #[serde(flatten)]
609        identity: EntryIdentityWire,
610        /// What the container reports about the entry's two sizes.
611        #[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
992// Each optional key below names itself in whatever it refuses.
993crate::named_optional_field!(deserialize_stored, u64, "stored");
994crate::named_optional_field!(deserialize_expanded, u64, "expanded");