Skip to main content

cadmpeg_core/
dialect.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Dialect identification: what a document is, as read from its own bytes.
3//!
4//! A dialect is a named region of one format's document space, bounded only by
5//! discriminants the format itself declares. It exists whether or not cadmpeg
6//! reads it. [`DialectMatch`] is the record of one run: inspect or decode
7//! handled the file, and [`Admission`] states how.
8//!
9//! This vocabulary is about the source document. cadmpeg's own version
10//! stamp, `IR_VERSION`, is data about cadmpeg, never merged with this one and
11//! never sharing a type with it.
12//!
13//! # `DialectId` is opaque here
14//!
15//! [`DialectId`] is a namespaced pinned string at this layer and nothing more:
16//! no shared enum of every format's dialects, no version parsing, no ordering.
17//! An IR-level enum of dialects would make the shared layer depend on every
18//! codec, and orderings are codec-local where they are real at all. Outside its
19//! owning codec the id is comparable and printable; only the owner parses it.
20//!
21//! Producers use [`dialect_id!`](crate::dialect_id) for checked static ids. Wire readers use
22//! [`DialectId::parse`] for checked owned ids. A codec backs its dialects with
23//! its own enum and returns registry-generated pinned constants from it — the
24//! `*LossCode` template: enum inside, pinned string at the boundary, one
25//! construction path, closed vocabulary. Deserialization is the only other way
26//! an id comes into being, and it reconstructs an id that some producer pinned.
27
28use std::borrow::Cow;
29use std::collections::BTreeMap;
30use std::error::Error;
31use std::fmt;
32
33use crate::decode::DecodeContext;
34use crate::CodecError;
35
36#[cfg(feature = "schema")]
37use schemars::JsonSchema;
38use serde::{Deserialize, Deserializer, Serialize, Serializer};
39
40use crate::text::NonBlankString;
41
42/// A registry dialect id, for example `"rhino:archive-80"`.
43///
44/// Canonical form is `<format>:<name>`. The format contains lowercase ASCII
45/// letters and digits. The name also admits dots and hyphens, but a hyphen
46/// cannot be first or last. Outside the owning codec the validated id is an
47/// opaque label.
48///
49/// Serializes and deserializes as the plain string. Read it with
50/// [`DialectId::as_str`] or print it; there is no other access to the raw
51/// string.
52#[derive(Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "String"))]
54pub struct DialectId {
55    value: Cow<'static, str>,
56    /// The separator found by admission. Both parts are non-empty ASCII.
57    namespace_len: usize,
58}
59
60/// A dialect id admitted from static storage.
61///
62/// This copyable proof lets a const initializer reject invalid text before
63/// constructing the borrowed-or-owned [`DialectId`].
64#[derive(Debug, Clone, Copy)]
65pub struct StaticDialectId {
66    value: &'static str,
67    namespace_len: usize,
68}
69
70impl StaticDialectId {
71    /// Admit a static id, returning `None` for noncanonical text.
72    #[must_use]
73    pub const fn new(id: &'static str) -> Option<Self> {
74        match dialect_separator(id) {
75            Some(namespace_len) => Some(Self {
76                value: id,
77                namespace_len,
78            }),
79            None => None,
80        }
81    }
82}
83
84impl DialectId {
85    /// Construct an id from admitted static text.
86    #[must_use]
87    pub const fn from_static(id: StaticDialectId) -> Self {
88        Self {
89            value: Cow::Borrowed(id.value),
90            namespace_len: id.namespace_len,
91        }
92    }
93
94    /// Parses and validates an owned dialect id.
95    pub fn parse(id: impl Into<String>) -> Result<Self, DialectIdError> {
96        let id = id.into();
97        match dialect_separator(&id) {
98            Some(namespace_len) => Ok(Self {
99                value: Cow::Owned(id),
100                namespace_len,
101            }),
102            None => Err(DialectIdError(id)),
103        }
104    }
105
106    /// Returns the id as a string slice.
107    #[must_use]
108    pub const fn as_str(&self) -> &str {
109        match &self.value {
110            Cow::Borrowed(id) => id,
111            Cow::Owned(id) => id.as_str(),
112        }
113    }
114
115    /// Returns the format namespace before the id's validated separator.
116    #[must_use]
117    pub fn namespace(&self) -> &str {
118        self.parts().0
119    }
120
121    pub(crate) fn local(&self) -> &str {
122        self.parts().1
123    }
124
125    fn parts(&self) -> (&str, &str) {
126        let (namespace, qualified_name) = self.as_str().split_at(self.namespace_len);
127        (namespace, &qualified_name[1..])
128    }
129}
130
131/// Construct a static dialect id whose grammar is checked during compilation.
132///
133/// ```compile_fail
134/// let _ = cadmpeg_core::dialect_id!("rhino:");
135/// ```
136#[macro_export]
137macro_rules! dialect_id {
138    ($literal:literal $(,)?) => {{
139        const STATIC_DIALECT_ID: $crate::dialect::StaticDialectId =
140            match $crate::dialect::StaticDialectId::new($literal) {
141                Some(id) => id,
142                None => panic!("invalid pinned dialect id"),
143            };
144        $crate::dialect::DialectId::from_static(STATIC_DIALECT_ID)
145    }};
146}
147
148const fn dialect_separator(id: &str) -> Option<usize> {
149    let bytes = id.as_bytes();
150    let mut index = 0;
151    let mut colon = None;
152    while index < bytes.len() {
153        if bytes[index] == b':' {
154            if colon.is_some() || index == 0 || index + 1 == bytes.len() {
155                return None;
156            }
157            colon = Some(index);
158        }
159        index += 1;
160    }
161    let Some(colon) = colon else {
162        return None;
163    };
164    index = 0;
165    while index < colon {
166        let byte = bytes[index];
167        if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() {
168            return None;
169        }
170        index += 1;
171    }
172    if valid_grammar_bytes(bytes, colon + 1) {
173        Some(colon)
174    } else {
175        None
176    }
177}
178
179/// States whether `bytes[start..]` is a format-local name.
180///
181/// The name is not empty, holds lowercase ASCII letters, digits, dots and
182/// hyphens only, and neither starts nor ends with a hyphen. A colon is outside
183/// the class, so a full `<format>:<name>` id is not a name.
184const fn valid_grammar_bytes(bytes: &[u8], start: usize) -> bool {
185    if start >= bytes.len() {
186        return false;
187    }
188    let mut index = start;
189    while index < bytes.len() {
190        let byte = bytes[index];
191        if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && byte != b'-' && byte != b'.' {
192            return false;
193        }
194        index += 1;
195    }
196    bytes[start] != b'-' && bytes[bytes.len() - 1] != b'-'
197}
198
199/// A string is not a canonical dialect id.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct DialectIdError(String);
202
203impl fmt::Display for DialectIdError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(
206            f,
207            "invalid dialect id {:?}: expected <format>:<name> in lowercase canonical form",
208            self.0
209        )
210    }
211}
212
213impl Error for DialectIdError {}
214
215impl fmt::Display for DialectId {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.write_str(self.as_str())
218    }
219}
220
221impl fmt::Debug for DialectId {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "DialectId({:?})", self.as_str())
224    }
225}
226
227impl Serialize for DialectId {
228    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
229        serializer.serialize_str(self.as_str())
230    }
231}
232
233impl<'de> Deserialize<'de> for DialectId {
234    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
235        Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
236    }
237}
238
239/// How one format layer admitted, or refused, one document.
240///
241/// Identity and admission are orthogonal: a document can carry a registry row
242/// of its own while its bytes are read with another grammar, and a damaged
243/// frame can retain its identity while applying that row's grammar
244/// unverified. The grammar name is local to the owning [`DialectMatch`]'s
245/// format namespace.
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[cfg_attr(feature = "schema", derive(JsonSchema))]
248#[serde(rename_all = "snake_case", deny_unknown_fields)]
249pub enum Admission {
250    /// Parsed with the strategy declared for the identified dialect.
251    Admitted,
252    /// Parsed with another declared grammar of the same format namespace.
253    ///
254    /// The codec must charge its dialect-unverified loss.
255    Unverified {
256        /// Format-local name of the grammar the parser applied.
257        using: Grammar,
258    },
259    /// Parsed without any declared grammar: the residual path.
260    ///
261    /// The codec must charge its dialect-unverified loss.
262    Residual,
263    /// Structurally identified; semantic decode refused.
264    Refused,
265}
266
267/// Format-local name of a dialect grammar, for example `sch-sw-33103`.
268///
269/// The namespace is the owning [`DialectMatch`]'s, so a grammar cannot name a
270/// foreign format by construction. The name holds the character class a
271/// [`DialectId`] admits after its separator, which excludes the separator
272/// itself: a full id has no spelling as a grammar name.
273///
274/// Serializes and deserializes as the plain string. `try_from` is the mint, so
275/// no read path reaches the field without the check. serde refuses
276/// `transparent` beside `try_from`; a newtype struct writes its inner string
277/// with no wrapper of its own, which is the same wire form.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[cfg_attr(feature = "schema", derive(JsonSchema))]
280#[serde(try_from = "String")]
281pub struct Grammar(String);
282
283impl Grammar {
284    /// Names the format-local half of a registry dialect id.
285    #[must_use]
286    pub fn of(dialect: &DialectId) -> Self {
287        Self(dialect.local().to_owned())
288    }
289
290    /// Parses and validates a format-local grammar name.
291    ///
292    /// The only read path into the type, through its `TryFrom<String>`.
293    pub fn parse(name: impl Into<String>) -> Result<Self, GrammarError> {
294        let name = name.into();
295        if valid_grammar_bytes(name.as_bytes(), 0) {
296            Ok(Self(name))
297        } else {
298            Err(GrammarError(name))
299        }
300    }
301
302    /// Returns the format-local grammar name.
303    #[must_use]
304    pub fn as_str(&self) -> &str {
305        &self.0
306    }
307}
308
309impl TryFrom<String> for Grammar {
310    type Error = GrammarError;
311
312    fn try_from(name: String) -> Result<Self, Self::Error> {
313        Self::parse(name)
314    }
315}
316
317/// A string is not a format-local grammar name.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct GrammarError(String);
320
321impl fmt::Display for GrammarError {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        write!(
324            f,
325            "invalid grammar name {:?}: expected a format-local name in lowercase canonical form",
326            self.0
327        )
328    }
329}
330
331impl Error for GrammarError {}
332
333/// One format layer's identification of one document.
334///
335/// A document carries several format layers: `.sldprt` contains Parasolid;
336/// `.f3d`, `.ipt`, and SAT contain ACIS; NX contains Parasolid and JT. One
337/// match describes one layer.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[cfg_attr(feature = "schema", derive(JsonSchema))]
340#[serde(deny_unknown_fields)]
341pub struct DialectMatch {
342    /// Registry dialect id, for example `"rhino:archive-80"`.
343    dialect: DialectId,
344    /// Version fields the source declared, verbatim, under keys pinned per
345    /// codec in the registry.
346    ///
347    /// Declarations are evidence, never a control input: the dialect is what
348    /// the bytes obey, not what they declare.
349    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
350    #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
351    declared: BTreeMap<NonBlankString, String>,
352    /// Instance of this format layer inside the containing document.
353    ///
354    /// `None` when the layer occurs once or has no report-local identity. This
355    /// is not source-declared evidence and therefore does not belong in
356    /// [`Self::declared`].
357    #[serde(
358        default,
359        skip_serializing_if = "Option::is_none",
360        deserialize_with = "deserialize_instance"
361    )]
362    instance: Option<String>,
363    /// How this layer was admitted.
364    admission: Admission,
365}
366
367/// Whether a format layer is the sole instance or needs its carrier identity.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum LayerInstance {
370    /// The host contains one layer of this format.
371    Sole,
372    /// The host contains several layers and the carrier disambiguates this one.
373    Tagged,
374}
375
376/// A report's primary format layer and any nested or carried format layers.
377///
378/// Extra layers are unique by `(format, instance)`. An extra layer for the
379/// primary format has an instance that identifies it inside the containing
380/// document. The wire names the primary explicitly, so the collection carries
381/// its complete identity without an enclosing report's format.
382///
383/// A read rejects a wire whose `extra` repeats a `(format, instance)` key, and
384/// one whose `extra` carries the primary key. The key set on the wire is the
385/// key set of the value, so no layer is silently discarded or overwritten.
386#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
387#[cfg_attr(feature = "schema", derive(JsonSchema))]
388pub struct DialectLayers {
389    primary: DialectMatch,
390    extra: Vec<DialectMatch>,
391}
392
393#[derive(Deserialize)]
394#[serde(deny_unknown_fields)]
395struct DialectLayersWire {
396    primary: DialectMatch,
397    extra: Vec<DialectMatch>,
398}
399
400impl<'de> Deserialize<'de> for DialectLayers {
401    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
402        let wire = DialectLayersWire::deserialize(deserializer)?;
403        let mut layers = Self::of(wire.primary);
404        for layer in wire.extra {
405            layers.insert(layer).map_err(|rejected| {
406                <D::Error as serde::de::Error>::custom(format!(
407                    "duplicate dialect layer key: {rejected:?}"
408                ))
409            })?;
410        }
411        Ok(layers)
412    }
413}
414
415/// Failure to insert a unique dialect layer.
416#[derive(Debug, Clone, PartialEq, Eq)]
417pub enum DialectLayerError {
418    /// The existing layer has the same format and instance.
419    Duplicate(DialectMatch),
420    /// The decode policy or allocator refused storage or work.
421    ResourceLimit(crate::decode::ResourceLimit),
422}
423
424impl fmt::Display for DialectLayerError {
425    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
426        match self {
427            Self::Duplicate(layer) => write!(formatter, "duplicate dialect layer key: {layer:?}"),
428            Self::ResourceLimit(limit) => {
429                write!(formatter, "resource refusal during {}", limit.operation)
430            }
431        }
432    }
433}
434
435impl Error for DialectLayerError {}
436
437impl From<DialectLayerError> for CodecError {
438    fn from(error: DialectLayerError) -> Self {
439        match error {
440            DialectLayerError::Duplicate(layer) => {
441                Self::malformed(format_args!("duplicate dialect layer key: {layer:?}"))
442            }
443            DialectLayerError::ResourceLimit(limit) => Self::ResourceLimit(limit),
444        }
445    }
446}
447
448impl DialectLayers {
449    /// Copy dialect layers and their owned storage through the decode budget.
450    pub fn try_clone_for_decode(
451        &self,
452        ctx: &DecodeContext<'_>,
453        operation: &'static str,
454    ) -> Result<Self, CodecError> {
455        let primary = self.primary.try_clone_for_decode(ctx, operation)?;
456        let mut extra = ctx.collection_vec(self.extra.len(), operation)?;
457        ctx.charge_work(crate::decode::u64_from_index(self.extra.len()), operation)?;
458        for layer in &self.extra {
459            extra.push(layer.try_clone_for_decode(ctx, operation)?);
460        }
461        Ok(Self { primary, extra })
462    }
463
464    /// Constructs dialect layers with one primary and no extra layers.
465    #[must_use]
466    pub fn of(primary: DialectMatch) -> Self {
467        Self {
468            primary,
469            extra: Vec::new(),
470        }
471    }
472
473    /// Inserts a layer keyed by `(format, instance)`.
474    ///
475    /// The first layer for a key remains authoritative. A duplicate is
476    /// returned unchanged so its producer can report the collision.
477    pub fn insert(&mut self, layer: DialectMatch) -> Result<(), DialectLayerError> {
478        let arena = crate::decode::DecodeArena::new();
479        let (ctx, _) = DecodeContext::from_root_bytes_limit(
480            &[],
481            &arena,
482            &crate::decode::DecodePolicy::default(),
483        )
484        .map_err(DialectLayerError::ResourceLimit)?;
485        self.insert_for_decode(&ctx, layer, "dialect layer insertion")
486    }
487
488    /// Insert one unique layer after admitting lookup work and retained storage.
489    pub fn insert_for_decode(
490        &mut self,
491        ctx: &DecodeContext<'_>,
492        layer: DialectMatch,
493        operation: &'static str,
494    ) -> Result<(), DialectLayerError> {
495        ctx.charge_work_limit(
496            crate::decode::u64_from_index(self.extra.len()) + 1,
497            operation,
498        )
499        .map_err(DialectLayerError::ResourceLimit)?;
500        if Self::same_key(&self.primary, &layer)
501            || self
502                .extra
503                .iter()
504                .any(|existing| Self::same_key(existing, &layer))
505        {
506            return Err(DialectLayerError::Duplicate(layer));
507        }
508        ctx.reserve_vec_limit(&mut self.extra, 1, operation)
509            .map_err(DialectLayerError::ResourceLimit)?;
510        self.extra.push(layer);
511        Ok(())
512    }
513
514    /// Adds a layer, returning it unchanged when its key is occupied.
515    pub fn with(mut self, layer: DialectMatch) -> Result<Self, DialectLayerError> {
516        self.insert(layer)?;
517        Ok(self)
518    }
519
520    /// Adds a layer through the caller's decode budget.
521    pub fn with_for_decode(
522        mut self,
523        ctx: &DecodeContext<'_>,
524        layer: DialectMatch,
525        operation: &'static str,
526    ) -> Result<Self, DialectLayerError> {
527        self.insert_for_decode(ctx, layer, operation)?;
528        Ok(self)
529    }
530
531    fn same_key(existing: &DialectMatch, layer: &DialectMatch) -> bool {
532        existing.format() == layer.format() && existing.instance == layer.instance
533    }
534
535    /// Returns the report's primary format layer.
536    #[must_use]
537    pub fn primary(&self) -> &DialectMatch {
538        &self.primary
539    }
540
541    /// Iterates over the primary layer followed by every extra layer.
542    pub fn iter(&self) -> impl Iterator<Item = &DialectMatch> {
543        std::iter::once(&self.primary).chain(&self.extra)
544    }
545}
546
547mod format_identity_sealed {
548    pub trait Sealed {}
549}
550
551/// A classified payload whose format namespace is intrinsic to the payload.
552///
553/// This trait is sealed. The supported payloads are [`DialectId`],
554/// [`DialectMatch`], and [`DialectLayers`].
555pub trait FormatIdentityPayload: format_identity_sealed::Sealed {
556    /// Returns the payload's authoritative format namespace.
557    fn format(&self) -> &str;
558}
559
560impl format_identity_sealed::Sealed for DialectId {}
561
562impl FormatIdentityPayload for DialectId {
563    fn format(&self) -> &str {
564        self.namespace()
565    }
566}
567
568impl format_identity_sealed::Sealed for DialectMatch {}
569
570impl FormatIdentityPayload for DialectMatch {
571    fn format(&self) -> &str {
572        self.format()
573    }
574}
575
576impl format_identity_sealed::Sealed for DialectLayers {}
577
578impl FormatIdentityPayload for DialectLayers {
579    fn format(&self) -> &str {
580        self.primary().format()
581    }
582}
583
584/// A format identity that is either classified by a typed payload or retains
585/// only its known format.
586///
587/// Classified state stores no second format string. The payload is the one
588/// author of its namespace, so the format is on the wire exactly once in
589/// either state.
590#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
591#[cfg_attr(feature = "schema", derive(JsonSchema))]
592#[serde(tag = "classification", rename_all = "snake_case", deny_unknown_fields)]
593pub enum FormatIdentity<T> {
594    /// The payload carries the complete classified identity.
595    Classified {
596        /// Classified payload, whose namespace is the format.
597        dialects: T,
598    },
599    /// The format is known but no classification payload exists.
600    Unclassified {
601        /// Registry format namespace.
602        format: String,
603    },
604}
605
606impl<T: FormatIdentityPayload> FormatIdentity<T> {
607    /// Constructs an identity whose format comes from its classified payload.
608    #[must_use]
609    pub const fn classified(payload: T) -> Self {
610        Self::Classified { dialects: payload }
611    }
612
613    /// Constructs an identity for a known format without classification.
614    #[must_use]
615    pub fn unclassified(format: impl Into<String>) -> Self {
616        Self::Unclassified {
617            format: format.into(),
618        }
619    }
620
621    /// Returns the authoritative format id.
622    #[must_use]
623    pub fn format(&self) -> &str {
624        match self {
625            Self::Classified { dialects } => dialects.format(),
626            Self::Unclassified { format } => format,
627        }
628    }
629
630    /// Returns the classified payload, when present.
631    #[must_use]
632    pub const fn classified_payload(&self) -> Option<&T> {
633        match self {
634            Self::Classified { dialects } => Some(dialects),
635            Self::Unclassified { .. } => None,
636        }
637    }
638}
639
640impl DialectMatch {
641    /// Copy a dialect layer into a decoded report under the caller's resource limits.
642    pub fn try_clone_for_decode(
643        &self,
644        ctx: &DecodeContext<'_>,
645        operation: &'static str,
646    ) -> Result<Self, crate::CodecError> {
647        let dialect = DialectId {
648            value: match &self.dialect.value {
649                Cow::Borrowed(value) => Cow::Borrowed(value),
650                Cow::Owned(value) => Cow::Owned(ctx.copy_retained_text(value, operation)?),
651            },
652            namespace_len: self.dialect.namespace_len,
653        };
654        let mut declared = BTreeMap::new();
655        for (key, value) in &self.declared {
656            ctx.admit_retained_btree_record::<NonBlankString, String>(0, operation)?;
657            let key = NonBlankString::new(ctx.copy_retained_text(key.as_str(), operation)?)
658                .ok_or_else(|| crate::CodecError::malformed("dialect declaration key is blank"))?;
659            declared.insert(key, ctx.copy_retained_text(value, operation)?);
660        }
661        let instance = self
662            .instance
663            .as_ref()
664            .map(|value| ctx.copy_retained_text(value, operation))
665            .transpose()?;
666        let admission = match &self.admission {
667            Admission::Admitted => Admission::Admitted,
668            Admission::Residual => Admission::Residual,
669            Admission::Refused => Admission::Refused,
670            Admission::Unverified { using } => Admission::Unverified {
671                using: Grammar(ctx.copy_retained_text(using.as_str(), operation)?),
672            },
673        };
674        Ok(Self {
675            dialect,
676            declared,
677            instance,
678            admission,
679        })
680    }
681
682    fn with_admission(dialect: DialectId, admission: Admission) -> Self {
683        Self {
684            dialect,
685            declared: BTreeMap::new(),
686            instance: None,
687            admission,
688        }
689    }
690
691    /// Constructs a layer parsed with its identified dialect's grammar.
692    #[must_use]
693    pub fn admitted(dialect: DialectId) -> Self {
694        Self::with_admission(dialect, Admission::Admitted)
695    }
696
697    /// Constructs a layer parsed unverified with another declared grammar.
698    ///
699    /// `using` supplies its format-local name; the grammar's namespace is the
700    /// layer's own.
701    #[must_use]
702    pub fn unverified(dialect: DialectId, using: Grammar) -> Self {
703        Self::with_admission(dialect, Admission::Unverified { using })
704    }
705
706    /// Constructs a layer parsed on the residual path without a declared grammar.
707    #[must_use]
708    pub fn residual(dialect: DialectId) -> Self {
709        Self::with_admission(dialect, Admission::Residual)
710    }
711
712    /// Constructs a structurally identified layer whose decode was refused.
713    #[must_use]
714    pub fn refused(dialect: DialectId) -> Self {
715        Self::with_admission(dialect, Admission::Refused)
716    }
717
718    /// Attaches source-declared version fields before the match enters a report.
719    #[must_use]
720    pub fn with_declared(mut self, declared: BTreeMap<NonBlankString, String>) -> Self {
721        self.declared = declared;
722        self
723    }
724
725    /// Adds one declaration after charging its map slot and owned value.
726    pub fn with_declared_entry(
727        mut self,
728        ctx: &crate::decode::DecodeContext<'_>,
729        key: NonBlankString,
730        value: &str,
731        operation: &'static str,
732    ) -> Result<Self, crate::CodecError> {
733        if !self.declared.contains_key(&key) {
734            ctx.admit_retained_btree_record::<NonBlankString, String>(0, operation)?;
735        }
736        let value = ctx.copy_retained_text(value, operation)?;
737        let previous = self.declared.insert(key, value);
738        drop(previous);
739        Ok(self)
740    }
741
742    /// Attaches a report-local layer instance before the match enters a report.
743    #[must_use]
744    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
745        self.instance = Some(instance.into());
746        self
747    }
748
749    /// Returns the classified format layer.
750    #[must_use]
751    pub fn format(&self) -> &str {
752        self.dialect.namespace()
753    }
754
755    /// Returns the registry dialect identity.
756    #[must_use]
757    pub fn dialect(&self) -> &DialectId {
758        &self.dialect
759    }
760
761    /// Returns the source-declared version fields.
762    #[must_use]
763    pub fn declared(&self) -> &BTreeMap<NonBlankString, String> {
764        &self.declared
765    }
766
767    /// Returns the report-local layer instance.
768    #[must_use]
769    pub fn instance(&self) -> Option<&str> {
770        self.instance.as_deref()
771    }
772
773    /// Returns how this layer was admitted.
774    #[must_use]
775    pub fn admission(&self) -> &Admission {
776        &self.admission
777    }
778
779    /// Returns the full registry id of the grammar applied, when this layer
780    /// was parsed unverified with a declared grammar.
781    #[must_use]
782    pub fn using(&self) -> Option<DialectId> {
783        match &self.admission {
784            Admission::Unverified { using } => Some(self.grammar_id(using)),
785            Admission::Admitted | Admission::Residual | Admission::Refused => None,
786        }
787    }
788
789    /// Return the full grammar identity in this match’s format namespace.
790    #[must_use]
791    pub fn grammar_id(&self, grammar: &Grammar) -> DialectId {
792        let namespace = self.format();
793        DialectId {
794            value: Cow::Owned(format!("{namespace}:{}", grammar.as_str())),
795            namespace_len: namespace.len(),
796        }
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    #![allow(clippy::unwrap_used)]
803
804    use std::collections::BTreeMap;
805
806    use crate::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
807    use crate::CodecError;
808
809    use super::{Admission, DialectId, DialectLayers, DialectMatch, Grammar, StaticDialectId};
810
811    #[derive(serde::Deserialize)]
812    #[serde(deny_unknown_fields)]
813    struct DialectIdConformance {
814        valid: Vec<String>,
815        invalid: Vec<String>,
816    }
817
818    fn layer(format: &str) -> DialectMatch {
819        DialectMatch::admitted(DialectId::parse(format!("{format}:known")).unwrap())
820    }
821
822    #[test]
823    fn charged_layer_copy_preserves_declarations_and_refuses_retained_limit() {
824        let primary = DialectMatch::admitted(crate::dialect_id!("sldprt:unknown")).with_declared(
825            std::collections::BTreeMap::from([(
826                crate::nonblank_literal!("sw_version"),
827                "12000".to_string(),
828            )]),
829        );
830        let extra =
831            DialectMatch::residual(crate::dialect_id!("parasolid:unknown")).with_instance("body-1");
832        let layers = DialectLayers::of(primary).with(extra).unwrap();
833        let arena = DecodeArena::new();
834        let policy = DecodePolicy::service();
835        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
836        assert_eq!(
837            layers
838                .try_clone_for_decode(&ctx, "copy dialect text")
839                .unwrap(),
840            layers
841        );
842
843        let limited_arena = DecodeArena::new();
844        let mut limited = DecodePolicy::service();
845        limited.limits.max_retained_bytes = 1;
846        let (ctx, _) = DecodeContext::from_root_bytes(&[], &limited_arena, &limited).unwrap();
847        let error = layers
848            .try_clone_for_decode(&ctx, "copy dialect text")
849            .unwrap_err();
850        assert!(matches!(
851            error,
852            crate::CodecError::ResourceLimit(limit)
853                if limit.dimension == ResourceDimension::RetainedBytes
854                    && limit.operation == "copy dialect text"
855        ));
856    }
857
858    #[test]
859    fn copied_dialect_layers_refuse_owned_identity_at_retained_limit() {
860        let layers = DialectLayers::of(layer("nx"));
861        let arena = DecodeArena::new();
862        let mut policy = DecodePolicy::service();
863        policy.limits.max_retained_bytes = 0;
864        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
865
866        assert!(matches!(
867            layers.try_clone_for_decode(&ctx, "dialect identity copy"),
868            Err(crate::CodecError::ResourceLimit(limit))
869                if limit.dimension == ResourceDimension::RetainedBytes
870                    && limit.operation == "dialect identity copy"
871        ));
872    }
873
874    #[test]
875    fn copied_dialect_layers_refuse_extra_layer_at_collection_limit() {
876        let mut layers = DialectLayers::of(layer("nx"));
877        layers.insert(layer("step")).unwrap();
878        let arena = DecodeArena::new();
879        let mut policy = DecodePolicy::service();
880        policy.limits.max_collection_items = 0;
881        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
882
883        assert!(matches!(
884            layers.try_clone_for_decode(&ctx, "dialect layer copies"),
885            Err(crate::CodecError::ResourceLimit(limit))
886                if limit.dimension == ResourceDimension::CollectionItems
887                    && limit.operation == "dialect layer copies"
888        ));
889    }
890
891    #[test]
892    fn dialect_extra_copy_admits_retained_vector_storage() {
893        let layers = DialectLayers::of(DialectMatch::admitted(crate::dialect_id!("nx:unknown")))
894            .with(DialectMatch::admitted(crate::dialect_id!("step:unknown")))
895            .unwrap();
896        let arena = DecodeArena::new();
897        let mut policy = DecodePolicy::service();
898        policy.limits.max_retained_bytes =
899            crate::decode::u64_from_index(std::mem::size_of::<DialectMatch>()) - 1;
900        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
901        assert!(
902            matches!(layers.try_clone_for_decode(&ctx, "copy extra storage"),
903            Err(CodecError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::RetainedBytes
904                && limit.operation == "copy extra storage")
905        );
906    }
907
908    #[test]
909    fn dialect_insertion_refuses_storage_without_changing_layers() {
910        let primary = DialectMatch::admitted(crate::dialect_id!("nx:unknown"));
911        let mut layers = DialectLayers::of(primary);
912        let arena = DecodeArena::new();
913        let mut policy = DecodePolicy::service();
914        policy.limits.max_retained_bytes =
915            crate::decode::u64_from_index(std::mem::size_of::<DialectMatch>()) - 1;
916        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
917        assert!(matches!(layers.insert_for_decode(&ctx,
918            DialectMatch::admitted(crate::dialect_id!("step:unknown")), "insert layer"),
919            Err(super::DialectLayerError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::RetainedBytes));
920        assert_eq!(layers.iter().count(), 1);
921    }
922
923    #[test]
924    fn dialect_declaration_copy_admits_map_node_storage() {
925        let layer = DialectMatch::admitted(crate::dialect_id!("nx:unknown")).with_declared(
926            std::collections::BTreeMap::from([(
927                crate::nonblank_literal!("version"),
928                String::new(),
929            )]),
930        );
931        let node_bytes = 11
932            * (std::mem::size_of::<crate::text::NonBlankString>() + std::mem::size_of::<String>())
933            + 16 * std::mem::size_of::<usize>()
934            + 2 * std::mem::align_of::<String>().max(std::mem::align_of::<usize>());
935        let arena = DecodeArena::new();
936        let mut policy = DecodePolicy::service();
937        // One declaration record admits one backing node before its text copies.
938        let bytes = node_bytes;
939        policy.limits.max_retained_bytes = crate::decode::u64_from_index(bytes) - 1;
940        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
941        assert!(
942            matches!(layer.try_clone_for_decode(&ctx, "copy declaration"),
943            Err(CodecError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::RetainedBytes
944                && limit.additional == crate::decode::u64_from_index(bytes))
945        );
946    }
947
948    #[test]
949    fn a_pinned_id_prints_and_serializes_as_the_plain_string() {
950        let id = crate::dialect_id!("rhino:archive-80");
951
952        assert_eq!(id.to_string(), "rhino:archive-80");
953        assert_eq!(id.as_str(), "rhino:archive-80");
954        assert_eq!(serde_json::to_string(&id).unwrap(), "\"rhino:archive-80\"");
955        assert_eq!(
956            serde_json::from_str::<DialectId>("\"rhino:archive-80\"").unwrap(),
957            id
958        );
959    }
960
961    #[test]
962    fn static_dialect_admission_returns_none_for_invalid_runtime_text() {
963        for text in [
964            "",
965            "rhino",
966            "rhino:",
967            ":known",
968            "Rhino:known",
969            "a:b:c",
970            "a:é",
971        ] {
972            assert!(StaticDialectId::new(text).is_none(), "{text:?}");
973        }
974        let admitted = DialectId::from_static(StaticDialectId::new("rhino:archive-80").unwrap());
975        assert_eq!(admitted.namespace(), "rhino");
976        assert_eq!(admitted.local(), "archive-80");
977        assert_eq!(admitted, DialectId::parse("rhino:archive-80").unwrap());
978    }
979
980    #[test]
981    fn dialect_id_deserialization_rejects_noncanonical_strings() {
982        for id in [
983            "rhino",
984            ":archive-80",
985            "rhino:",
986            "Rhino:archive-80",
987            "rhino:archive_80",
988            "rhino:-archive-80",
989        ] {
990            serde_json::from_value::<DialectId>(serde_json::json!(id))
991                .expect_err("a malformed dialect id must be rejected");
992        }
993    }
994
995    #[test]
996    fn dialect_id_matches_the_shared_conformance_corpus() {
997        let cases: DialectIdConformance =
998            toml::from_str(include_str!("../../../docs/dialect-id-conformance.toml")).unwrap();
999        for id in cases.valid {
1000            let admitted =
1001                DialectId::parse(id.clone()).unwrap_or_else(|_| panic!("valid dialect id {id:?}"));
1002            assert_eq!(format!("{}:{}", admitted.namespace(), admitted.local()), id);
1003        }
1004        for id in cases.invalid {
1005            assert!(
1006                DialectId::parse(id.clone()).is_err(),
1007                "invalid dialect id {id:?}"
1008            );
1009        }
1010    }
1011
1012    #[test]
1013    fn an_admitted_match_serializes_its_identity() {
1014        let admitted = DialectMatch {
1015            dialect: crate::dialect_id!("rhino:archive-80"),
1016            declared: BTreeMap::new(),
1017            instance: None,
1018            admission: Admission::Admitted,
1019        };
1020
1021        assert_eq!(
1022            serde_json::to_string(&admitted).unwrap(),
1023            "{\"dialect\":\"rhino:archive-80\",\"admission\":\"admitted\"}"
1024        );
1025    }
1026
1027    #[test]
1028    fn a_dialect_match_wire_states_its_format_once_through_its_dialect() {
1029        let restated = serde_json::json!({
1030            "format": "rhino",
1031            "dialect": "rhino:archive-80",
1032            "admission": "admitted",
1033        });
1034
1035        let error = serde_json::from_value::<DialectMatch>(restated)
1036            .expect_err("the format is read from the dialect, so the wire carries no format key");
1037        assert!(
1038            error.to_string().contains("unknown field `format`"),
1039            "{error}"
1040        );
1041        assert_eq!(
1042            DialectMatch::admitted(crate::dialect_id!("rhino:archive-80")).format(),
1043            "rhino"
1044        );
1045    }
1046
1047    #[test]
1048    fn residual_constructor_records_the_absence_of_a_declared_grammar() {
1049        let residual = DialectMatch::residual(crate::dialect_id!("rhino:unknown"));
1050
1051        assert_eq!(residual.admission(), &Admission::Residual);
1052        assert_eq!(residual.using(), None);
1053        assert_eq!(
1054            serde_json::to_string(&residual).unwrap(),
1055            "{\"dialect\":\"rhino:unknown\",\"admission\":\"residual\"}"
1056        );
1057    }
1058
1059    #[test]
1060    fn an_unverified_admission_names_the_grammar_in_use_by_full_id() {
1061        let unverified = DialectMatch::unverified(
1062            crate::dialect_id!("acis:save-format-217"),
1063            Grammar::of(&crate::dialect_id!("acis:save-format-218")),
1064        );
1065
1066        assert_eq!(
1067            unverified.admission(),
1068            &Admission::Unverified {
1069                using: Grammar::of(&crate::dialect_id!("acis:save-format-218")),
1070            }
1071        );
1072        assert_eq!(
1073            unverified.using(),
1074            Some(crate::dialect_id!("acis:save-format-218"))
1075        );
1076        let serialized = serde_json::to_string(&unverified).unwrap();
1077        assert_eq!(
1078            serialized,
1079            "{\"dialect\":\"acis:save-format-217\",\"admission\":{\"unverified\":{\"using\":\"save-format-218\"}}}"
1080        );
1081        assert_eq!(
1082            serde_json::from_str::<DialectMatch>(&serialized).unwrap(),
1083            unverified
1084        );
1085    }
1086
1087    #[test]
1088    fn a_self_named_unverified_grammar_remains_opaque() {
1089        let self_named = serde_json::json!({
1090            "dialect": "rhino:unknown",
1091            "admission": {
1092                "unverified": { "using": "unknown" }
1093            },
1094        });
1095        let matched = serde_json::from_value::<DialectMatch>(self_named)
1096            .expect("core does not infer grammar semantics from an opaque dialect name");
1097        assert_eq!(
1098            matched.admission(),
1099            &Admission::Unverified {
1100                using: Grammar::of(&crate::dialect_id!("rhino:unknown")),
1101            }
1102        );
1103    }
1104
1105    #[test]
1106    fn a_grammar_name_has_no_spelling_for_a_foreign_namespace() {
1107        let malformed = serde_json::json!({
1108            "dialect": "rhino:unknown",
1109            "admission": {
1110                "unverified": { "using": "step:ap242-e3" }
1111            },
1112        });
1113        let error = serde_json::from_value::<DialectMatch>(malformed)
1114            .expect_err("a grammar substitute belongs to the classified format layer");
1115        assert!(
1116            error
1117                .to_string()
1118                .contains("invalid grammar name \"step:ap242-e3\""),
1119            "{error}"
1120        );
1121    }
1122
1123    #[test]
1124    fn grammar_parsing_rejects_a_name_outside_the_format_local_class() {
1125        for name in [
1126            "",
1127            "rhino:archive-80",
1128            "Archive-80",
1129            "archive_80",
1130            "-archive",
1131        ] {
1132            assert!(
1133                Grammar::parse(name).is_err(),
1134                "invalid grammar name {name:?}"
1135            );
1136        }
1137        assert_eq!(
1138            Grammar::parse("save-format-218").unwrap(),
1139            Grammar::of(&crate::dialect_id!("acis:save-format-218"))
1140        );
1141    }
1142
1143    #[test]
1144    fn identity_does_not_encode_whether_an_unverified_path_used_a_grammar() {
1145        let dialect = crate::dialect_id!("rhino:archive-80");
1146        let without_grammar = DialectMatch::residual(dialect.clone());
1147        assert_eq!(without_grammar.admission(), &Admission::Residual);
1148
1149        let self_named = DialectMatch::unverified(dialect.clone(), Grammar::of(&dialect));
1150        assert_eq!(
1151            self_named.admission(),
1152            &Admission::Unverified {
1153                using: Grammar::of(&crate::dialect_id!("rhino:archive-80")),
1154            }
1155        );
1156        assert_eq!(self_named.using(), Some(dialect));
1157    }
1158
1159    #[test]
1160    fn dialect_layers_accept_a_same_format_extra_with_an_instance() {
1161        let member = layer("rhino").with_instance("components/member.3dm");
1162        let layers = DialectLayers::of(layer("rhino"))
1163            .with(member.clone())
1164            .expect("distinct dialect layer keys");
1165
1166        let serialized = serde_json::to_value(&layers).unwrap();
1167        assert_eq!(
1168            serde_json::from_value::<DialectLayers>(serialized).unwrap(),
1169            layers
1170        );
1171        assert_eq!(layers.extra, [member]);
1172    }
1173
1174    #[test]
1175    fn dialect_layers_insert_keeps_the_first_extra_layer_for_a_key() {
1176        let first = layer("acis").with_instance("body");
1177        let replacement =
1178            DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
1179        let mut layers = DialectLayers::of(layer("rhino"))
1180            .with(first.clone())
1181            .expect("distinct dialect layer keys");
1182
1183        assert_eq!(
1184            layers.insert(replacement.clone()),
1185            Err(super::DialectLayerError::Duplicate(replacement))
1186        );
1187        assert_eq!(layers.extra, [first]);
1188    }
1189
1190    #[test]
1191    fn dialect_layers_insert_keeps_the_primary_on_its_own_key() {
1192        let replacement = DialectMatch::residual(crate::dialect_id!("rhino:other"));
1193        let mut layers = DialectLayers::of(layer("rhino"))
1194            .with(layer("acis"))
1195            .expect("distinct dialect layer keys");
1196
1197        assert_eq!(
1198            layers.insert(replacement.clone()),
1199            Err(super::DialectLayerError::Duplicate(replacement))
1200        );
1201        assert_eq!(layers.primary(), &layer("rhino"));
1202        assert_eq!(layers.extra, [layer("acis")]);
1203    }
1204
1205    #[test]
1206    fn dialect_layers_builder_returns_the_colliding_layer() {
1207        let first = layer("acis").with_instance("body");
1208        let replacement =
1209            DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
1210        let layers = DialectLayers::of(layer("rhino"))
1211            .with(first)
1212            .expect("distinct dialect layer keys");
1213        assert_eq!(
1214            layers.with(replacement.clone()),
1215            Err(super::DialectLayerError::Duplicate(replacement))
1216        );
1217    }
1218
1219    #[test]
1220    fn dialect_layers_keep_same_format_extras_with_distinct_instances() {
1221        let anonymous = layer("acis");
1222        let named = layer("acis").with_instance("body");
1223        let mut layers = DialectLayers::of(layer("rhino"));
1224
1225        assert_eq!(layers.insert(anonymous.clone()), Ok(()));
1226        assert_eq!(layers.insert(named.clone()), Ok(()));
1227        assert_eq!(layers.extra, [anonymous, named]);
1228    }
1229
1230    #[test]
1231    fn dialect_layers_deserialization_rejects_a_repeated_key() {
1232        let serialized = serde_json::json!({
1233            "primary": layer("rhino"),
1234            "extra": [layer("acis"), DialectMatch::residual(crate::dialect_id!("acis:other"))],
1235        });
1236
1237        let error = serde_json::from_value::<DialectLayers>(serialized)
1238            .expect_err("a repeated extra layer key is a read error");
1239        assert!(
1240            error.to_string().contains("duplicate dialect layer key"),
1241            "{error}"
1242        );
1243    }
1244
1245    #[test]
1246    fn dialect_layers_deserialization_rejects_an_extra_on_the_primary_key() {
1247        let serialized = serde_json::json!({
1248            "primary": layer("rhino"),
1249            "extra": [DialectMatch::residual(crate::dialect_id!("rhino:other"))],
1250        });
1251
1252        let error = serde_json::from_value::<DialectLayers>(serialized)
1253            .expect_err("an extra layer on the primary key is a read error");
1254        assert!(
1255            error.to_string().contains("duplicate dialect layer key"),
1256            "{error}"
1257        );
1258    }
1259
1260    #[test]
1261    fn dialect_layers_serialize_with_an_explicit_primary() {
1262        let layers = DialectLayers::of(layer("rhino"))
1263            .with(layer("acis"))
1264            .expect("distinct dialect layer keys");
1265        let serialized = serde_json::to_value(&layers).unwrap();
1266
1267        assert_eq!(
1268            serialized,
1269            serde_json::json!({
1270                "primary": layer("rhino"),
1271                "extra": [layer("acis")],
1272            })
1273        );
1274        assert_eq!(
1275            serde_json::from_value::<DialectLayers>(serialized).unwrap(),
1276            layers
1277        );
1278        assert_eq!(
1279            layers.iter().map(DialectMatch::format).collect::<Vec<_>>(),
1280            ["rhino", "acis"]
1281        );
1282    }
1283}
1284
1285// Each optional key below names itself in whatever it refuses.
1286crate::named_optional_field!(deserialize_instance, String, "instance");