1use crate::tls_map::TlsMap;
2use prost::Message;
3use tls_codec::{Deserialize, Serialize, VLBytes};
4use xmtp_proto::xmtp::mls::message_contents::{
5 ComponentMetadata, ComponentPermissions, ComponentType, MetadataPolicy as MetadataPolicyProto,
6 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
7};
8
9use super::component_id::ComponentId;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ComponentOp {
18 Insert,
20 Update,
22 Delete,
24}
25
26impl std::fmt::Display for ComponentOp {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 ComponentOp::Insert => write!(f, "insert"),
30 ComponentOp::Update => write!(f, "update"),
31 ComponentOp::Delete => write!(f, "delete"),
32 }
33 }
34}
35
36pub fn new_component_metadata(
38 permissions: ComponentPermissions,
39 component_type: ComponentType,
40) -> ComponentMetadata {
41 ComponentMetadata {
42 permissions: Some(permissions),
43 component_type: component_type as i32,
44 external_committer_permissions: None,
45 }
46}
47
48#[derive(Debug, thiserror::Error)]
49pub enum ComponentRegistryError {
50 #[error("component ID {0} is not in the component ID space")]
51 InvalidComponentId(ComponentId),
52 #[error("component ID {0} is in the reserved range")]
53 ReservedRange(ComponentId),
54 #[error("immutable component {0} cannot be modified after initial insert")]
55 ImmutableComponent(ComponentId),
56 #[error("hardcoded component {0} cannot be removed")]
57 HardcodedComponent(ComponentId),
58 #[error("component {0} not found")]
59 NotFound(ComponentId),
60 #[error("component {0} metadata is missing the permissions field")]
61 MissingPermissions(ComponentId),
62 #[error("component {0} metadata is missing the {1} policy field")]
63 MissingPolicyField(ComponentId, ComponentOp),
64 #[error("decode error for component {component_id}: {source}")]
65 DecodeError {
66 component_id: ComponentId,
67 #[source]
68 source: prost::DecodeError,
69 },
70 #[error("tls codec error: {0}")]
71 TlsCodecError(#[from] tls_codec::Error),
72 #[error("constrained component {0} requires AllowIfAdmin or AllowIfSuperAdmin policies")]
73 ConstrainedPolicyViolation(ComponentId),
74}
75
76#[derive(Debug, Clone, PartialEq)]
122pub struct ComponentRegistry {
123 inner: TlsMap<ComponentId, VLBytes>,
124}
125
126impl ComponentRegistry {
127 pub fn new() -> Self {
128 Self {
129 inner: TlsMap::new(),
130 }
131 }
132
133 pub fn get(
141 &self,
142 id: &ComponentId,
143 ) -> Result<Option<ComponentMetadata>, ComponentRegistryError> {
144 Ok(self
145 .inner
146 .get(id)
147 .and_then(|raw| Self::decode_recognized(*id, raw.as_slice()).ok()))
148 }
149
150 pub fn set(
167 &mut self,
168 id: ComponentId,
169 meta: ComponentMetadata,
170 ) -> Result<(), ComponentRegistryError> {
171 self.validate_modifiable(&id)?;
172 Self::validate_metadata(&id, &meta)?;
173 let bytes = VLBytes::new(meta.encode_to_vec());
174 self.inner.set(id, bytes);
175 Ok(())
176 }
177
178 pub fn remove(&mut self, id: &ComponentId) -> Result<(), ComponentRegistryError> {
186 self.validate_modifiable(id)?;
187 self.inner
188 .remove(id)
189 .map_err(|_| ComponentRegistryError::NotFound(*id))?;
190 Ok(())
191 }
192
193 pub fn contains(&self, id: &ComponentId) -> bool {
197 self.inner
198 .get(id)
199 .is_some_and(|raw| Self::decode_recognized(*id, raw.as_slice()).is_ok())
200 }
201
202 pub fn contains_raw(&self, id: &ComponentId) -> bool {
208 self.inner.contains_key(id)
209 }
210
211 pub fn len(&self) -> usize {
214 self.iter().count()
215 }
216
217 pub fn is_empty(&self) -> bool {
220 self.iter().next().is_none()
221 }
222
223 pub fn iter(
232 &self,
233 ) -> impl Iterator<Item = Result<(ComponentId, ComponentMetadata), ComponentRegistryError>> + '_
234 {
235 self.inner.iter().filter_map(|(&id, raw)| {
236 Self::decode_recognized(id, raw.as_slice())
237 .ok()
238 .map(|meta| Ok((id, meta)))
239 })
240 }
241
242 pub fn to_bytes(&self) -> Result<Vec<u8>, ComponentRegistryError> {
256 Ok(self.inner.tls_serialize_detached()?)
257 }
258
259 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ComponentRegistryError> {
277 let inner = TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(bytes)?;
278 Ok(Self { inner })
279 }
280
281 pub fn unrecognized_ids(&self) -> impl Iterator<Item = ComponentId> + '_ {
287 self.inner.iter().filter_map(|(&id, raw)| {
288 Self::decode_recognized(id, raw.as_slice())
289 .is_err()
290 .then_some(id)
291 })
292 }
293
294 pub fn validate_entry(id: ComponentId, raw: &[u8]) -> Result<(), ComponentRegistryError> {
301 Self::decode_recognized(id, raw).map(|_| ())
302 }
303
304 fn decode_recognized(
317 id: ComponentId,
318 raw: &[u8],
319 ) -> Result<ComponentMetadata, ComponentRegistryError> {
320 if !id.is_in_component_space() {
321 return Err(ComponentRegistryError::InvalidComponentId(id));
322 }
323 if id.is_reserved() {
324 return Err(ComponentRegistryError::ReservedRange(id));
325 }
326 if id.is_hardcoded() {
327 return Err(ComponentRegistryError::HardcodedComponent(id));
328 }
329 let meta = ComponentMetadata::decode(raw).map_err(|source| {
330 ComponentRegistryError::DecodeError {
331 component_id: id,
332 source,
333 }
334 })?;
335 Self::validate_metadata(&id, &meta)?;
336 Ok(meta)
337 }
338
339 fn validate_modifiable(&self, id: &ComponentId) -> Result<(), ComponentRegistryError> {
354 if !id.is_in_component_space() {
355 return Err(ComponentRegistryError::InvalidComponentId(*id));
356 }
357 if id.is_reserved() {
358 return Err(ComponentRegistryError::ReservedRange(*id));
359 }
360 if id.is_hardcoded() {
361 return Err(ComponentRegistryError::HardcodedComponent(*id));
362 }
363 if id.is_immutable() && self.contains_raw(id) {
364 return Err(ComponentRegistryError::ImmutableComponent(*id));
365 }
366 Ok(())
367 }
368
369 fn validate_metadata(
383 id: &ComponentId,
384 meta: &ComponentMetadata,
385 ) -> Result<(), ComponentRegistryError> {
386 let perms = meta
387 .permissions
388 .as_ref()
389 .ok_or(ComponentRegistryError::MissingPermissions(*id))?;
390
391 for (op, policy) in [
392 (ComponentOp::Insert, &perms.insert_policy),
393 (ComponentOp::Update, &perms.update_policy),
394 (ComponentOp::Delete, &perms.delete_policy),
395 ] {
396 let p = policy
397 .as_ref()
398 .ok_or(ComponentRegistryError::MissingPolicyField(*id, op))?;
399
400 if id.is_constrained() && !Self::is_admin_or_super_admin_or_deny_policy(p) {
401 return Err(ComponentRegistryError::ConstrainedPolicyViolation(*id));
402 }
403 }
404 Ok(())
405 }
406
407 fn is_admin_or_super_admin_or_deny_policy(policy: &MetadataPolicyProto) -> bool {
416 match &policy.kind {
417 Some(MetadataPolicyKind::Base(base)) => {
418 *base == MetadataBasePolicy::Deny as i32
424 || *base == MetadataBasePolicy::AllowIfAdmin as i32
425 || *base == MetadataBasePolicy::AllowIfSuperAdmin as i32
426 }
427 Some(MetadataPolicyKind::AndCondition(_)) => false,
431 Some(MetadataPolicyKind::AnyCondition(_)) => false,
432 None => false,
433 }
434 }
435}
436
437impl Default for ComponentRegistry {
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::app_data::component_permissions::component_permissions;
447 use xmtp_proto::xmtp::mls::message_contents::metadata_policy::{AndCondition, AnyCondition};
448
449 fn allow() -> MetadataPolicyProto {
450 MetadataPolicyProto {
451 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Allow as i32)),
452 }
453 }
454
455 fn deny() -> MetadataPolicyProto {
456 MetadataPolicyProto {
457 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Deny as i32)),
458 }
459 }
460
461 fn admin_only() -> MetadataPolicyProto {
462 MetadataPolicyProto {
463 kind: Some(MetadataPolicyKind::Base(
464 MetadataBasePolicy::AllowIfAdmin as i32,
465 )),
466 }
467 }
468
469 fn and_condition_admin_only() -> MetadataPolicyProto {
473 MetadataPolicyProto {
474 kind: Some(MetadataPolicyKind::AndCondition(AndCondition {
475 policies: vec![admin_only(), admin_only()],
476 })),
477 }
478 }
479
480 fn any_condition_admin_only() -> MetadataPolicyProto {
483 MetadataPolicyProto {
484 kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
485 policies: vec![admin_only(), admin_only()],
486 })),
487 }
488 }
489
490 fn sample_meta() -> ComponentMetadata {
491 new_component_metadata(
492 component_permissions()
493 .insert(allow())
494 .update(admin_only())
495 .delete(deny())
496 .call(),
497 ComponentType::Bytes,
498 )
499 }
500
501 #[xmtp_common::test]
502 fn test_set_and_get() {
503 let mut reg = ComponentRegistry::new();
504 let id = ComponentId::GROUP_NAME;
505 reg.set(id, sample_meta()).unwrap();
506 let meta = reg.get(&id).unwrap().unwrap();
507 assert_eq!(meta, sample_meta());
508 }
509
510 #[xmtp_common::test]
511 fn test_set_overwrites() {
512 let mut reg = ComponentRegistry::new();
513 let id = ComponentId::GROUP_NAME;
514 reg.set(id, sample_meta()).unwrap();
515
516 let new_meta = new_component_metadata(
517 component_permissions()
518 .insert(deny())
519 .update(deny())
520 .delete(deny())
521 .call(),
522 ComponentType::TlsMapBytesBytes,
523 );
524 reg.set(id, new_meta.clone()).unwrap();
525
526 let got = reg.get(&id).unwrap().unwrap();
527 assert_eq!(got, new_meta);
528 assert_eq!(reg.len(), 1);
529 }
530
531 #[xmtp_common::test]
532 fn test_remove() {
533 let mut reg = ComponentRegistry::new();
534 let id = ComponentId::GROUP_NAME;
535 reg.set(id, sample_meta()).unwrap();
536 reg.remove(&id).unwrap();
537 assert!(!reg.contains(&id));
538 }
539
540 #[xmtp_common::test]
541 fn test_remove_missing_returns_error() {
542 let mut reg = ComponentRegistry::new();
543 let result = reg.remove(&ComponentId::GROUP_NAME);
544 assert!(result.is_err());
545 }
546
547 #[xmtp_common::test(unwrap_try = true)]
548 fn test_reject_hardcoded_set() {
549 let mut reg = ComponentRegistry::new();
554 assert!(matches!(
555 reg.set(ComponentId::COMPONENT_REGISTRY, sample_meta()),
556 Err(ComponentRegistryError::HardcodedComponent(_))
557 ));
558 assert!(matches!(
559 reg.set(ComponentId::SUPER_ADMIN_LIST, sample_meta()),
560 Err(ComponentRegistryError::HardcodedComponent(_))
561 ));
562 assert!(reg.is_empty());
563 }
564
565 #[xmtp_common::test(unwrap_try = true)]
566 fn test_reject_hardcoded_remove() {
567 let mut reg = ComponentRegistry::new();
570 assert!(matches!(
571 reg.remove(&ComponentId::COMPONENT_REGISTRY),
572 Err(ComponentRegistryError::HardcodedComponent(_))
573 ));
574 assert!(matches!(
575 reg.remove(&ComponentId::SUPER_ADMIN_LIST),
576 Err(ComponentRegistryError::HardcodedComponent(_))
577 ));
578 }
579
580 #[xmtp_common::test]
581 fn test_admin_list_accepts_admin_or_super_admin_policy() {
582 let mut reg = ComponentRegistry::new();
583 let meta = new_component_metadata(
584 component_permissions()
585 .insert(admin_only())
586 .update(admin_only())
587 .delete(admin_only())
588 .call(),
589 ComponentType::Bytes,
590 );
591 assert!(reg.set(ComponentId::ADMIN_LIST, meta).is_ok());
592 }
593
594 #[xmtp_common::test]
595 fn test_admin_list_accepts_super_admin_only_policy() {
596 let mut reg = ComponentRegistry::new();
597 let super_admin_only = MetadataPolicyProto {
598 kind: Some(MetadataPolicyKind::Base(
599 MetadataBasePolicy::AllowIfSuperAdmin as i32,
600 )),
601 };
602 let meta = new_component_metadata(
603 component_permissions()
604 .insert(super_admin_only.clone())
605 .update(super_admin_only.clone())
606 .delete(super_admin_only)
607 .call(),
608 ComponentType::Bytes,
609 );
610 assert!(reg.set(ComponentId::ADMIN_LIST, meta).is_ok());
611 }
612
613 #[xmtp_common::test]
614 fn test_admin_list_rejects_allow_policy() {
615 let mut reg = ComponentRegistry::new();
616 let meta = new_component_metadata(
617 component_permissions()
618 .insert(allow())
619 .update(allow())
620 .delete(allow())
621 .call(),
622 ComponentType::Bytes,
623 );
624 assert!(matches!(
625 reg.set(ComponentId::ADMIN_LIST, meta),
626 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
627 ));
628 }
629
630 #[xmtp_common::test(unwrap_try = true)]
631 fn test_admin_list_accepts_deny_policy() {
632 let mut reg = ComponentRegistry::new();
633 let meta = new_component_metadata(
634 component_permissions()
635 .insert(admin_only())
636 .update(deny())
637 .delete(admin_only())
638 .call(),
639 ComponentType::Bytes,
640 );
641 assert!(reg.set(ComponentId::ADMIN_LIST, meta).is_ok());
642 }
643
644 #[xmtp_common::test]
645 fn test_admin_list_rejects_mixed_invalid_policy() {
646 let mut reg = ComponentRegistry::new();
647 let meta = new_component_metadata(
648 component_permissions()
649 .insert(admin_only())
650 .update(allow())
651 .delete(admin_only())
652 .call(),
653 ComponentType::Bytes,
654 );
655 assert!(matches!(
656 reg.set(ComponentId::ADMIN_LIST, meta),
657 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
658 ));
659 }
660
661 #[xmtp_common::test]
662 fn test_admin_list_rejects_and_condition_policy() {
663 let mut reg = ComponentRegistry::new();
670 let meta = new_component_metadata(
671 component_permissions()
672 .insert(and_condition_admin_only())
673 .update(admin_only())
674 .delete(admin_only())
675 .call(),
676 ComponentType::Bytes,
677 );
678 assert!(matches!(
679 reg.set(ComponentId::ADMIN_LIST, meta),
680 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
681 ));
682 }
683
684 #[xmtp_common::test]
685 fn test_admin_list_rejects_any_condition_policy() {
686 let mut reg = ComponentRegistry::new();
691 let meta = new_component_metadata(
692 component_permissions()
693 .insert(admin_only())
694 .update(any_condition_admin_only())
695 .delete(admin_only())
696 .call(),
697 ComponentType::Bytes,
698 );
699 assert!(matches!(
700 reg.set(ComponentId::ADMIN_LIST, meta),
701 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
702 ));
703 }
704
705 #[xmtp_common::test]
706 fn test_rejects_missing_permissions() {
708 let mut reg = ComponentRegistry::new();
709 let meta = ComponentMetadata {
713 permissions: None,
714 external_committer_permissions: None,
715 component_type: ComponentType::Bytes as i32,
716 };
717 assert!(matches!(
719 reg.set(ComponentId::GROUP_NAME, meta.clone()),
720 Err(ComponentRegistryError::MissingPermissions(_))
721 ));
722 assert!(matches!(
723 reg.set(ComponentId::ADMIN_LIST, meta),
724 Err(ComponentRegistryError::MissingPermissions(_))
725 ));
726 }
727
728 #[xmtp_common::test]
729 fn test_rejects_missing_policy_field() {
731 let mut reg = ComponentRegistry::new();
732 let meta = new_component_metadata(
733 ComponentPermissions {
734 insert_policy: Some(allow()),
735 update_policy: None, delete_policy: Some(allow()),
737 },
738 ComponentType::Bytes,
739 );
740 assert!(matches!(
742 reg.set(ComponentId::GROUP_NAME, meta),
743 Err(ComponentRegistryError::MissingPolicyField(
744 _,
745 ComponentOp::Update
746 ))
747 ));
748 }
749
750 #[xmtp_common::test]
751 fn test_reject_reserved_set() {
752 let mut reg = ComponentRegistry::new();
753 assert!(matches!(
754 reg.set(ComponentId::new(0xFF00), sample_meta()),
755 Err(ComponentRegistryError::ReservedRange(_))
756 ));
757 }
758
759 #[xmtp_common::test]
760 fn test_reject_invalid_id() {
761 let mut reg = ComponentRegistry::new();
762 assert!(matches!(
763 reg.set(ComponentId::new(0x0001), sample_meta()),
764 Err(ComponentRegistryError::InvalidComponentId(_))
765 ));
766 }
767
768 #[xmtp_common::test]
769 fn test_app_range_allowed() {
770 let mut reg = ComponentRegistry::new();
771 let id = ComponentId::new(0xC000);
772 reg.set(id, sample_meta()).unwrap();
773 assert!(reg.contains(&id));
774 }
775
776 #[xmtp_common::test]
777 fn test_immutable_first_insert_allowed() {
778 let mut reg = ComponentRegistry::new();
780 let id = ComponentId::new(0xBE00);
781 reg.set(id, sample_meta()).unwrap();
782 assert!(reg.contains(&id));
783 }
784
785 #[xmtp_common::test]
786 fn test_immutable_subsequent_set_rejected() {
787 let mut reg = ComponentRegistry::new();
788 let id = ComponentId::new(0xBE00);
789 reg.set(id, sample_meta()).unwrap();
790 assert!(matches!(
792 reg.set(id, sample_meta()),
793 Err(ComponentRegistryError::ImmutableComponent(_))
794 ));
795 }
796
797 #[xmtp_common::test]
798 fn test_immutable_remove_rejected() {
799 let mut reg = ComponentRegistry::new();
800 let id = ComponentId::new(0xBE00);
801 reg.set(id, sample_meta()).unwrap();
802 assert!(matches!(
803 reg.remove(&id),
804 Err(ComponentRegistryError::ImmutableComponent(_))
805 ));
806 assert!(reg.contains(&id));
807 }
808
809 #[xmtp_common::test]
810 fn test_tls_round_trip() {
811 let mut reg = ComponentRegistry::new();
812 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
813 reg.set(
814 ComponentId::GROUP_DESCRIPTION,
815 new_component_metadata(
816 component_permissions()
817 .insert(deny())
818 .update(deny())
819 .delete(deny())
820 .call(),
821 ComponentType::Bytes,
822 ),
823 )
824 .unwrap();
825
826 let bytes = reg.to_bytes().unwrap();
827 let restored = ComponentRegistry::from_bytes(&bytes).unwrap();
828 assert_eq!(reg, restored);
829 }
830
831 #[xmtp_common::test]
832 fn test_iter() {
833 let mut reg = ComponentRegistry::new();
834 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
835 reg.set(ComponentId::GROUP_DESCRIPTION, sample_meta())
836 .unwrap();
837
838 let entries: Vec<_> = reg.iter().collect::<Result<Vec<_>, _>>().unwrap();
839 assert_eq!(entries.len(), 2);
840 assert!(entries[0].0 < entries[1].0);
841 }
842
843 #[xmtp_common::test]
844 fn test_empty_registry_round_trip() {
845 let reg = ComponentRegistry::new();
846 let bytes = reg.to_bytes().unwrap();
847 let restored = ComponentRegistry::from_bytes(&bytes).unwrap();
848 assert_eq!(reg, restored);
849 assert!(restored.is_empty());
850 }
851
852 fn assert_tolerated(bytes: &[u8], id: ComponentId) {
856 let reg = ComponentRegistry::from_bytes(bytes).unwrap();
857 assert!(reg.get(&id).unwrap().is_none());
858 assert!(!reg.contains(&id));
859 assert_eq!(reg.len(), 0);
860 assert!(reg.iter().next().is_none());
861 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![id]);
862 assert_eq!(reg.to_bytes().unwrap(), bytes);
864 }
865
866 #[xmtp_common::test]
867 fn test_from_bytes_tolerates_out_of_space_id() {
868 let id = ComponentId::new(0x0001);
872 let bytes = raw_bytes_with_entry(id, sample_meta().encode_to_vec());
873 assert_tolerated(&bytes, id);
874 }
875
876 #[xmtp_common::test]
877 fn test_from_bytes_tolerates_reserved_id() {
878 let id = ComponentId::new(0xFF50);
882 let bytes = raw_bytes_with_entry(id, sample_meta().encode_to_vec());
883 assert_tolerated(&bytes, id);
884 }
885
886 fn raw_bytes_with_entry(id: ComponentId, value: Vec<u8>) -> Vec<u8> {
891 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
892 map.insert(id, VLBytes::new(value)).unwrap();
893 map.tls_serialize_detached().unwrap()
894 }
895
896 #[xmtp_common::test]
897 fn test_from_bytes_tolerates_hardcoded_id() {
898 let bytes = raw_bytes_with_entry(
902 ComponentId::COMPONENT_REGISTRY,
903 sample_meta().encode_to_vec(),
904 );
905 assert_tolerated(&bytes, ComponentId::COMPONENT_REGISTRY);
906
907 let bytes =
908 raw_bytes_with_entry(ComponentId::SUPER_ADMIN_LIST, sample_meta().encode_to_vec());
909 assert_tolerated(&bytes, ComponentId::SUPER_ADMIN_LIST);
910 }
911
912 #[xmtp_common::test]
913 fn test_from_bytes_tolerates_missing_permissions() {
914 let meta = ComponentMetadata {
919 permissions: None,
920 external_committer_permissions: None,
921 component_type: ComponentType::Bytes as i32,
922 };
923 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, meta.encode_to_vec());
924 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
925 }
926
927 #[xmtp_common::test]
928 fn test_from_bytes_tolerates_missing_policy_field() {
929 let meta = new_component_metadata(
931 ComponentPermissions {
932 insert_policy: Some(allow()),
933 update_policy: None,
934 delete_policy: Some(allow()),
935 },
936 ComponentType::Bytes,
937 );
938 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, meta.encode_to_vec());
939 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
940 }
941
942 #[xmtp_common::test]
943 fn test_from_bytes_tolerates_constrained_violation() {
944 let meta = new_component_metadata(
949 component_permissions()
950 .insert(allow())
951 .update(allow())
952 .delete(allow())
953 .call(),
954 ComponentType::Bytes,
955 );
956 let bytes = raw_bytes_with_entry(ComponentId::ADMIN_LIST, meta.encode_to_vec());
957 assert_tolerated(&bytes, ComponentId::ADMIN_LIST);
958 }
959
960 #[xmtp_common::test]
961 fn test_reads_skip_entry_with_undecodable_value() {
962 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
968 map.insert(
969 ComponentId::GROUP_NAME,
970 VLBytes::new(sample_meta().encode_to_vec()),
971 )
972 .unwrap();
973 map.insert(
974 ComponentId::GROUP_DESCRIPTION,
975 VLBytes::new(vec![0xFF, 0xFF, 0xFF, 0xFF]),
976 )
977 .unwrap();
978 let bytes = map.tls_serialize_detached().unwrap();
979
980 let reg = ComponentRegistry::from_bytes(&bytes).unwrap();
981
982 let entries: Vec<_> = reg.iter().collect::<Result<Vec<_>, _>>().unwrap();
983 assert_eq!(entries.len(), 1);
984 assert_eq!(entries[0].0, ComponentId::GROUP_NAME);
985 assert_eq!(
986 reg.get(&ComponentId::GROUP_NAME).unwrap(),
987 Some(sample_meta())
988 );
989 assert!(reg.get(&ComponentId::GROUP_DESCRIPTION).unwrap().is_none());
990 assert_eq!(
991 reg.unrecognized_ids().collect::<Vec<_>>(),
992 vec![ComponentId::GROUP_DESCRIPTION]
993 );
994 assert_eq!(reg.to_bytes().unwrap(), bytes);
995 }
996
997 #[xmtp_common::test]
998 fn test_from_bytes_tolerates_malformed_protobuf_value() {
1000 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, vec![0xFF, 0xFF, 0xFF, 0xFF]);
1004 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
1005 }
1006
1007 #[xmtp_common::test]
1008 fn test_from_bytes_rejects_undecodable_outer_map() {
1009 let mut bytes =
1012 raw_bytes_with_entry(ComponentId::GROUP_NAME, sample_meta().encode_to_vec());
1013 bytes.truncate(bytes.len() - 1);
1014 assert!(matches!(
1015 ComponentRegistry::from_bytes(&bytes),
1016 Err(ComponentRegistryError::TlsCodecError(_))
1017 ));
1018 }
1019
1020 #[xmtp_common::test]
1021 fn test_from_bytes_mixed_valid_and_invalid_entries() {
1023 let reserved = ComponentId::new(0xFF00);
1027 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
1028 map.insert(
1029 ComponentId::GROUP_NAME,
1030 VLBytes::new(sample_meta().encode_to_vec()),
1031 )
1032 .unwrap();
1033 map.insert(reserved, VLBytes::new(b"future-slot".to_vec()))
1034 .unwrap();
1035 let bytes = map.tls_serialize_detached().unwrap();
1036
1037 let reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1038 assert_eq!(reg.len(), 1);
1039 assert_eq!(
1040 reg.get(&ComponentId::GROUP_NAME).unwrap(),
1041 Some(sample_meta())
1042 );
1043 assert!(!reg.contains(&reserved));
1044 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![reserved]);
1045 assert_eq!(reg.to_bytes().unwrap(), bytes);
1046 }
1047
1048 #[xmtp_common::test(unwrap_try = true)]
1065 fn test_poisoned_registry_still_validates_unrelated_writes() {
1067 use crate::app_data::validation::{
1068 ActorAuthority, ComponentChange, ComponentPermissionError, validate_component_write,
1069 };
1070
1071 let poisoned = ComponentId::new(0xFF20);
1076 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
1077 map.insert(
1078 ComponentId::GROUP_NAME,
1079 VLBytes::new(sample_meta().encode_to_vec()),
1080 )?;
1081 map.insert(poisoned, VLBytes::new(vec![0xDE, 0xAD, 0xBE, 0xEF]))?;
1082 let bytes = map.tls_serialize_detached()?;
1083
1084 let reg = ComponentRegistry::from_bytes(&bytes)?;
1086 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![poisoned]);
1087
1088 assert_eq!(reg.get(&ComponentId::GROUP_NAME)?, Some(sample_meta()));
1091 ComponentRegistry::validate_entry(ComponentId::GROUP_NAME, &sample_meta().encode_to_vec())?;
1092
1093 let member = ActorAuthority {
1096 is_admin: false,
1097 is_super_admin: false,
1098 };
1099 let healthy_write = ComponentChange::builder()
1100 .component_id(ComponentId::GROUP_NAME)
1101 .op(ComponentOp::Insert)
1102 .actor(member)
1103 .build();
1104 validate_component_write(&healthy_write, ®)?;
1105
1106 let poisoned_write = ComponentChange::builder()
1109 .component_id(poisoned)
1110 .op(ComponentOp::Insert)
1111 .actor(member)
1112 .build();
1113 assert!(matches!(
1114 validate_component_write(&poisoned_write, ®),
1115 Err(ComponentPermissionError::NoRegistryEntry(id)) if id == poisoned
1116 ));
1117
1118 assert_eq!(reg.to_bytes()?, bytes);
1121 }
1122
1123 #[xmtp_common::test]
1124 fn test_set_repairs_unrecognized_entry() {
1125 let broken = ComponentMetadata {
1129 permissions: None,
1130 external_committer_permissions: None,
1131 component_type: ComponentType::Bytes as i32,
1132 };
1133 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, broken.encode_to_vec());
1134 let mut reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1135 assert_eq!(reg.unrecognized_ids().count(), 1);
1136
1137 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
1138 assert_eq!(reg.unrecognized_ids().count(), 0);
1139 assert_eq!(
1140 reg.get(&ComponentId::GROUP_NAME).unwrap(),
1141 Some(sample_meta())
1142 );
1143
1144 let round_tripped = ComponentRegistry::from_bytes(®.to_bytes().unwrap()).unwrap();
1145 assert_eq!(round_tripped.unrecognized_ids().count(), 0);
1146 assert_eq!(
1147 round_tripped.get(&ComponentId::GROUP_NAME).unwrap(),
1148 Some(sample_meta())
1149 );
1150 }
1151
1152 #[xmtp_common::test]
1156 fn test_set_refuses_unrecognized_immutable_entry() {
1158 let id = ComponentId::new(0xFD00);
1159 let bytes = raw_bytes_with_entry(id, vec![0xFF, 0xFF]);
1160 let mut reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1161 assert!(!reg.contains(&id));
1162 assert!(matches!(
1163 reg.set(id, sample_meta()),
1164 Err(ComponentRegistryError::ImmutableComponent(_))
1165 ));
1166 assert_eq!(reg.to_bytes().unwrap(), bytes);
1167 }
1168
1169 #[xmtp_common::test]
1172 fn test_contains_raw_sees_unrecognized_entry() {
1174 let id = ComponentId::new(0xC001);
1175 let reg = ComponentRegistry::from_bytes(&raw_bytes_with_entry(id, vec![0xFF])).unwrap();
1176 assert!(!reg.contains(&id));
1177 assert!(reg.contains_raw(&id));
1178 assert!(!reg.contains_raw(&ComponentId::new(0xC002)));
1179 }
1180
1181 #[xmtp_common::test]
1182 fn test_remove_deletes_unrecognized_entry() {
1183 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, vec![0xFF, 0xFF]);
1186 let mut reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1187 assert_eq!(reg.unrecognized_ids().count(), 1);
1188
1189 reg.remove(&ComponentId::GROUP_NAME).unwrap();
1190 assert_eq!(reg.unrecognized_ids().count(), 0);
1191 let empty = ComponentRegistry::new();
1192 assert_eq!(reg.to_bytes().unwrap(), empty.to_bytes().unwrap());
1193 }
1194
1195 #[xmtp_common::test]
1196 fn test_preserves_component_type() {
1197 let mut reg = ComponentRegistry::new();
1198 let meta = new_component_metadata(
1199 component_permissions()
1200 .insert(allow())
1201 .update(allow())
1202 .delete(deny())
1203 .call(),
1204 ComponentType::TlsMapBytesBytes,
1205 );
1206 reg.set(ComponentId::GROUP_MEMBERSHIP, meta).unwrap();
1207
1208 let got = reg.get(&ComponentId::GROUP_MEMBERSHIP).unwrap().unwrap();
1209 assert_eq!(got.component_type, ComponentType::TlsMapBytesBytes as i32);
1210 }
1211}