1use 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#[derive(Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "String"))]
54pub struct DialectId {
55 value: Cow<'static, str>,
56 namespace_len: usize,
58}
59
60#[derive(Debug, Clone, Copy)]
65pub struct StaticDialectId {
66 value: &'static str,
67 namespace_len: usize,
68}
69
70impl StaticDialectId {
71 #[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 #[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 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 #[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 #[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#[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
179const 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#[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#[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 Admitted,
252 Unverified {
256 using: Grammar,
258 },
259 Residual,
263 Refused,
265}
266
267#[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 #[must_use]
286 pub fn of(dialect: &DialectId) -> Self {
287 Self(dialect.local().to_owned())
288 }
289
290 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 #[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[cfg_attr(feature = "schema", derive(JsonSchema))]
340#[serde(deny_unknown_fields)]
341pub struct DialectMatch {
342 dialect: DialectId,
344 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
350 #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
351 declared: BTreeMap<NonBlankString, String>,
352 #[serde(
358 default,
359 skip_serializing_if = "Option::is_none",
360 deserialize_with = "deserialize_instance"
361 )]
362 instance: Option<String>,
363 admission: Admission,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum LayerInstance {
370 Sole,
372 Tagged,
374}
375
376#[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#[derive(Debug, Clone, PartialEq, Eq)]
417pub enum DialectLayerError {
418 Duplicate(DialectMatch),
420 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 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 #[must_use]
466 pub fn of(primary: DialectMatch) -> Self {
467 Self {
468 primary,
469 extra: Vec::new(),
470 }
471 }
472
473 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 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 pub fn with(mut self, layer: DialectMatch) -> Result<Self, DialectLayerError> {
516 self.insert(layer)?;
517 Ok(self)
518 }
519
520 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 #[must_use]
537 pub fn primary(&self) -> &DialectMatch {
538 &self.primary
539 }
540
541 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
551pub trait FormatIdentityPayload: format_identity_sealed::Sealed {
556 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#[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 Classified {
596 dialects: T,
598 },
599 Unclassified {
601 format: String,
603 },
604}
605
606impl<T: FormatIdentityPayload> FormatIdentity<T> {
607 #[must_use]
609 pub const fn classified(payload: T) -> Self {
610 Self::Classified { dialects: payload }
611 }
612
613 #[must_use]
615 pub fn unclassified(format: impl Into<String>) -> Self {
616 Self::Unclassified {
617 format: format.into(),
618 }
619 }
620
621 #[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 #[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 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 #[must_use]
693 pub fn admitted(dialect: DialectId) -> Self {
694 Self::with_admission(dialect, Admission::Admitted)
695 }
696
697 #[must_use]
702 pub fn unverified(dialect: DialectId, using: Grammar) -> Self {
703 Self::with_admission(dialect, Admission::Unverified { using })
704 }
705
706 #[must_use]
708 pub fn residual(dialect: DialectId) -> Self {
709 Self::with_admission(dialect, Admission::Residual)
710 }
711
712 #[must_use]
714 pub fn refused(dialect: DialectId) -> Self {
715 Self::with_admission(dialect, Admission::Refused)
716 }
717
718 #[must_use]
720 pub fn with_declared(mut self, declared: BTreeMap<NonBlankString, String>) -> Self {
721 self.declared = declared;
722 self
723 }
724
725 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 #[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 #[must_use]
751 pub fn format(&self) -> &str {
752 self.dialect.namespace()
753 }
754
755 #[must_use]
757 pub fn dialect(&self) -> &DialectId {
758 &self.dialect
759 }
760
761 #[must_use]
763 pub fn declared(&self) -> &BTreeMap<NonBlankString, String> {
764 &self.declared
765 }
766
767 #[must_use]
769 pub fn instance(&self) -> Option<&str> {
770 self.instance.as_deref()
771 }
772
773 #[must_use]
775 pub fn admission(&self) -> &Admission {
776 &self.admission
777 }
778
779 #[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 #[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 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
1285crate::named_optional_field!(deserialize_instance, String, "instance");