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(
166 &mut self,
167 id: ComponentId,
168 meta: ComponentMetadata,
169 ) -> Result<(), ComponentRegistryError> {
170 self.validate_modifiable(&id)?;
171 Self::validate_metadata(&id, &meta)?;
172 let bytes = VLBytes::new(meta.encode_to_vec());
173 self.inner.set(id, bytes);
174 Ok(())
175 }
176
177 pub fn remove(&mut self, id: &ComponentId) -> Result<(), ComponentRegistryError> {
185 self.validate_modifiable(id)?;
186 self.inner
187 .remove(id)
188 .map_err(|_| ComponentRegistryError::NotFound(*id))?;
189 Ok(())
190 }
191
192 pub fn contains(&self, id: &ComponentId) -> bool {
196 self.inner
197 .get(id)
198 .is_some_and(|raw| Self::decode_recognized(*id, raw.as_slice()).is_ok())
199 }
200
201 pub fn len(&self) -> usize {
204 self.iter().count()
205 }
206
207 pub fn is_empty(&self) -> bool {
210 self.iter().next().is_none()
211 }
212
213 pub fn iter(
222 &self,
223 ) -> impl Iterator<Item = Result<(ComponentId, ComponentMetadata), ComponentRegistryError>> + '_
224 {
225 self.inner.iter().filter_map(|(&id, raw)| {
226 Self::decode_recognized(id, raw.as_slice())
227 .ok()
228 .map(|meta| Ok((id, meta)))
229 })
230 }
231
232 pub fn to_bytes(&self) -> Result<Vec<u8>, ComponentRegistryError> {
245 Ok(self.inner.tls_serialize_detached()?)
246 }
247
248 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ComponentRegistryError> {
266 let inner = TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(bytes)?;
267 Ok(Self { inner })
268 }
269
270 pub fn unrecognized_ids(&self) -> impl Iterator<Item = ComponentId> + '_ {
276 self.inner.iter().filter_map(|(&id, raw)| {
277 Self::decode_recognized(id, raw.as_slice())
278 .is_err()
279 .then_some(id)
280 })
281 }
282
283 pub fn validate_entry(id: ComponentId, raw: &[u8]) -> Result<(), ComponentRegistryError> {
290 Self::decode_recognized(id, raw).map(|_| ())
291 }
292
293 fn decode_recognized(
305 id: ComponentId,
306 raw: &[u8],
307 ) -> Result<ComponentMetadata, ComponentRegistryError> {
308 if !id.is_in_component_space() {
309 return Err(ComponentRegistryError::InvalidComponentId(id));
310 }
311 if id.is_reserved() {
312 return Err(ComponentRegistryError::ReservedRange(id));
313 }
314 if id.is_hardcoded() {
315 return Err(ComponentRegistryError::HardcodedComponent(id));
316 }
317 let meta = ComponentMetadata::decode(raw).map_err(|source| {
318 ComponentRegistryError::DecodeError {
319 component_id: id,
320 source,
321 }
322 })?;
323 Self::validate_metadata(&id, &meta)?;
324 Ok(meta)
325 }
326
327 fn validate_modifiable(&self, id: &ComponentId) -> Result<(), ComponentRegistryError> {
341 if !id.is_in_component_space() {
342 return Err(ComponentRegistryError::InvalidComponentId(*id));
343 }
344 if id.is_reserved() {
345 return Err(ComponentRegistryError::ReservedRange(*id));
346 }
347 if id.is_hardcoded() {
348 return Err(ComponentRegistryError::HardcodedComponent(*id));
349 }
350 if id.is_immutable() && self.contains(id) {
351 return Err(ComponentRegistryError::ImmutableComponent(*id));
352 }
353 Ok(())
354 }
355
356 fn validate_metadata(
370 id: &ComponentId,
371 meta: &ComponentMetadata,
372 ) -> Result<(), ComponentRegistryError> {
373 let perms = meta
374 .permissions
375 .as_ref()
376 .ok_or(ComponentRegistryError::MissingPermissions(*id))?;
377
378 for (op, policy) in [
379 (ComponentOp::Insert, &perms.insert_policy),
380 (ComponentOp::Update, &perms.update_policy),
381 (ComponentOp::Delete, &perms.delete_policy),
382 ] {
383 let p = policy
384 .as_ref()
385 .ok_or(ComponentRegistryError::MissingPolicyField(*id, op))?;
386
387 if id.is_constrained() && !Self::is_admin_or_super_admin_policy(p) {
388 return Err(ComponentRegistryError::ConstrainedPolicyViolation(*id));
389 }
390 }
391 Ok(())
392 }
393
394 fn is_admin_or_super_admin_policy(policy: &MetadataPolicyProto) -> bool {
402 match &policy.kind {
403 Some(MetadataPolicyKind::Base(base)) => {
404 *base == MetadataBasePolicy::AllowIfAdmin as i32
405 || *base == MetadataBasePolicy::AllowIfSuperAdmin as i32
406 }
407 Some(MetadataPolicyKind::AndCondition(_)) => false,
411 Some(MetadataPolicyKind::AnyCondition(_)) => false,
412 None => false,
413 }
414 }
415}
416
417impl Default for ComponentRegistry {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::app_data::component_permissions::component_permissions;
427 use xmtp_proto::xmtp::mls::message_contents::metadata_policy::{AndCondition, AnyCondition};
428
429 fn allow() -> MetadataPolicyProto {
430 MetadataPolicyProto {
431 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Allow as i32)),
432 }
433 }
434
435 fn deny() -> MetadataPolicyProto {
436 MetadataPolicyProto {
437 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Deny as i32)),
438 }
439 }
440
441 fn admin_only() -> MetadataPolicyProto {
442 MetadataPolicyProto {
443 kind: Some(MetadataPolicyKind::Base(
444 MetadataBasePolicy::AllowIfAdmin as i32,
445 )),
446 }
447 }
448
449 fn and_condition_admin_only() -> MetadataPolicyProto {
453 MetadataPolicyProto {
454 kind: Some(MetadataPolicyKind::AndCondition(AndCondition {
455 policies: vec![admin_only(), admin_only()],
456 })),
457 }
458 }
459
460 fn any_condition_admin_only() -> MetadataPolicyProto {
463 MetadataPolicyProto {
464 kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
465 policies: vec![admin_only(), admin_only()],
466 })),
467 }
468 }
469
470 fn sample_meta() -> ComponentMetadata {
471 new_component_metadata(
472 component_permissions()
473 .insert(allow())
474 .update(admin_only())
475 .delete(deny())
476 .call(),
477 ComponentType::Bytes,
478 )
479 }
480
481 #[xmtp_common::test]
482 fn test_set_and_get() {
483 let mut reg = ComponentRegistry::new();
484 let id = ComponentId::GROUP_NAME;
485 reg.set(id, sample_meta()).unwrap();
486 let meta = reg.get(&id).unwrap().unwrap();
487 assert_eq!(meta, sample_meta());
488 }
489
490 #[xmtp_common::test]
491 fn test_get_missing_returns_none() {
492 let reg = ComponentRegistry::new();
493 assert!(reg.get(&ComponentId::GROUP_NAME).unwrap().is_none());
494 }
495
496 #[xmtp_common::test]
497 fn test_set_overwrites() {
498 let mut reg = ComponentRegistry::new();
499 let id = ComponentId::GROUP_NAME;
500 reg.set(id, sample_meta()).unwrap();
501
502 let new_meta = new_component_metadata(
503 component_permissions()
504 .insert(deny())
505 .update(deny())
506 .delete(deny())
507 .call(),
508 ComponentType::TlsMapBytesBytes,
509 );
510 reg.set(id, new_meta.clone()).unwrap();
511
512 let got = reg.get(&id).unwrap().unwrap();
513 assert_eq!(got, new_meta);
514 assert_eq!(reg.len(), 1);
515 }
516
517 #[xmtp_common::test]
518 fn test_remove() {
519 let mut reg = ComponentRegistry::new();
520 let id = ComponentId::GROUP_NAME;
521 reg.set(id, sample_meta()).unwrap();
522 reg.remove(&id).unwrap();
523 assert!(!reg.contains(&id));
524 }
525
526 #[xmtp_common::test]
527 fn test_remove_missing_returns_error() {
528 let mut reg = ComponentRegistry::new();
529 let result = reg.remove(&ComponentId::GROUP_NAME);
530 assert!(result.is_err());
531 }
532
533 #[xmtp_common::test]
534 fn test_reject_hardcoded_set() {
535 let mut reg = ComponentRegistry::new();
540 assert!(matches!(
541 reg.set(ComponentId::COMPONENT_REGISTRY, sample_meta()),
542 Err(ComponentRegistryError::HardcodedComponent(_))
543 ));
544 assert!(matches!(
545 reg.set(ComponentId::SUPER_ADMIN_LIST, sample_meta()),
546 Err(ComponentRegistryError::HardcodedComponent(_))
547 ));
548 assert!(reg.is_empty());
549 }
550
551 #[xmtp_common::test]
552 fn test_reject_hardcoded_remove() {
553 let mut reg = ComponentRegistry::new();
556 assert!(matches!(
557 reg.remove(&ComponentId::COMPONENT_REGISTRY),
558 Err(ComponentRegistryError::HardcodedComponent(_))
559 ));
560 assert!(matches!(
561 reg.remove(&ComponentId::SUPER_ADMIN_LIST),
562 Err(ComponentRegistryError::HardcodedComponent(_))
563 ));
564 }
565
566 #[xmtp_common::test]
567 fn test_admin_list_accepts_admin_or_super_admin_policy() {
568 let mut reg = ComponentRegistry::new();
569 let meta = new_component_metadata(
570 component_permissions()
571 .insert(admin_only())
572 .update(admin_only())
573 .delete(admin_only())
574 .call(),
575 ComponentType::Bytes,
576 );
577 assert!(reg.set(ComponentId::ADMIN_LIST, meta).is_ok());
578 }
579
580 #[xmtp_common::test]
581 fn test_admin_list_accepts_super_admin_only_policy() {
582 let mut reg = ComponentRegistry::new();
583 let super_admin_only = MetadataPolicyProto {
584 kind: Some(MetadataPolicyKind::Base(
585 MetadataBasePolicy::AllowIfSuperAdmin as i32,
586 )),
587 };
588 let meta = new_component_metadata(
589 component_permissions()
590 .insert(super_admin_only.clone())
591 .update(super_admin_only.clone())
592 .delete(super_admin_only)
593 .call(),
594 ComponentType::Bytes,
595 );
596 assert!(reg.set(ComponentId::ADMIN_LIST, meta).is_ok());
597 }
598
599 #[xmtp_common::test]
600 fn test_admin_list_rejects_allow_policy() {
601 let mut reg = ComponentRegistry::new();
602 let meta = new_component_metadata(
603 component_permissions()
604 .insert(allow())
605 .update(allow())
606 .delete(allow())
607 .call(),
608 ComponentType::Bytes,
609 );
610 assert!(matches!(
611 reg.set(ComponentId::ADMIN_LIST, meta),
612 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
613 ));
614 }
615
616 #[xmtp_common::test]
617 fn test_admin_list_rejects_deny_policy() {
618 let mut reg = ComponentRegistry::new();
619 let meta = new_component_metadata(
620 component_permissions()
621 .insert(admin_only())
622 .update(deny())
623 .delete(admin_only())
624 .call(),
625 ComponentType::Bytes,
626 );
627 assert!(matches!(
628 reg.set(ComponentId::ADMIN_LIST, meta),
629 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
630 ));
631 }
632
633 #[xmtp_common::test]
634 fn test_admin_list_rejects_mixed_invalid_policy() {
635 let mut reg = ComponentRegistry::new();
636 let meta = new_component_metadata(
637 component_permissions()
638 .insert(admin_only())
639 .update(allow())
640 .delete(admin_only())
641 .call(),
642 ComponentType::Bytes,
643 );
644 assert!(matches!(
645 reg.set(ComponentId::ADMIN_LIST, meta),
646 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
647 ));
648 }
649
650 #[xmtp_common::test]
651 fn test_admin_list_rejects_and_condition_policy() {
652 let mut reg = ComponentRegistry::new();
658 let meta = new_component_metadata(
659 component_permissions()
660 .insert(and_condition_admin_only())
661 .update(admin_only())
662 .delete(admin_only())
663 .call(),
664 ComponentType::Bytes,
665 );
666 assert!(matches!(
667 reg.set(ComponentId::ADMIN_LIST, meta),
668 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
669 ));
670 }
671
672 #[xmtp_common::test]
673 fn test_admin_list_rejects_any_condition_policy() {
674 let mut reg = ComponentRegistry::new();
679 let meta = new_component_metadata(
680 component_permissions()
681 .insert(admin_only())
682 .update(any_condition_admin_only())
683 .delete(admin_only())
684 .call(),
685 ComponentType::Bytes,
686 );
687 assert!(matches!(
688 reg.set(ComponentId::ADMIN_LIST, meta),
689 Err(ComponentRegistryError::ConstrainedPolicyViolation(_))
690 ));
691 }
692
693 #[xmtp_common::test]
694 fn test_rejects_missing_permissions() {
695 let mut reg = ComponentRegistry::new();
696 let meta = ComponentMetadata {
700 permissions: None,
701 external_committer_permissions: None,
702 component_type: ComponentType::Bytes as i32,
703 };
704 assert!(matches!(
706 reg.set(ComponentId::GROUP_NAME, meta.clone()),
707 Err(ComponentRegistryError::MissingPermissions(_))
708 ));
709 assert!(matches!(
710 reg.set(ComponentId::ADMIN_LIST, meta),
711 Err(ComponentRegistryError::MissingPermissions(_))
712 ));
713 }
714
715 #[xmtp_common::test]
716 fn test_rejects_missing_policy_field() {
717 let mut reg = ComponentRegistry::new();
718 let meta = new_component_metadata(
719 ComponentPermissions {
720 insert_policy: Some(allow()),
721 update_policy: None, delete_policy: Some(allow()),
723 },
724 ComponentType::Bytes,
725 );
726 assert!(matches!(
728 reg.set(ComponentId::GROUP_NAME, meta),
729 Err(ComponentRegistryError::MissingPolicyField(
730 _,
731 ComponentOp::Update
732 ))
733 ));
734 }
735
736 #[xmtp_common::test]
737 fn test_reject_reserved_set() {
738 let mut reg = ComponentRegistry::new();
739 assert!(matches!(
740 reg.set(ComponentId::new(0xFF00), sample_meta()),
741 Err(ComponentRegistryError::ReservedRange(_))
742 ));
743 }
744
745 #[xmtp_common::test]
746 fn test_reject_invalid_id() {
747 let mut reg = ComponentRegistry::new();
748 assert!(matches!(
749 reg.set(ComponentId::new(0x0001), sample_meta()),
750 Err(ComponentRegistryError::InvalidComponentId(_))
751 ));
752 }
753
754 #[xmtp_common::test]
755 fn test_app_range_allowed() {
756 let mut reg = ComponentRegistry::new();
757 let id = ComponentId::new(0xC000);
758 reg.set(id, sample_meta()).unwrap();
759 assert!(reg.contains(&id));
760 }
761
762 #[xmtp_common::test]
763 fn test_immutable_first_insert_allowed() {
764 let mut reg = ComponentRegistry::new();
766 let id = ComponentId::new(0xBE00);
767 reg.set(id, sample_meta()).unwrap();
768 assert!(reg.contains(&id));
769 }
770
771 #[xmtp_common::test]
772 fn test_immutable_subsequent_set_rejected() {
773 let mut reg = ComponentRegistry::new();
774 let id = ComponentId::new(0xBE00);
775 reg.set(id, sample_meta()).unwrap();
776 assert!(matches!(
778 reg.set(id, sample_meta()),
779 Err(ComponentRegistryError::ImmutableComponent(_))
780 ));
781 }
782
783 #[xmtp_common::test]
784 fn test_immutable_remove_rejected() {
785 let mut reg = ComponentRegistry::new();
786 let id = ComponentId::new(0xBE00);
787 reg.set(id, sample_meta()).unwrap();
788 assert!(matches!(
789 reg.remove(&id),
790 Err(ComponentRegistryError::ImmutableComponent(_))
791 ));
792 assert!(reg.contains(&id));
793 }
794
795 #[xmtp_common::test]
796 fn test_tls_round_trip() {
797 let mut reg = ComponentRegistry::new();
798 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
799 reg.set(
800 ComponentId::GROUP_DESCRIPTION,
801 new_component_metadata(
802 component_permissions()
803 .insert(deny())
804 .update(deny())
805 .delete(deny())
806 .call(),
807 ComponentType::Bytes,
808 ),
809 )
810 .unwrap();
811
812 let bytes = reg.to_bytes().unwrap();
813 let restored = ComponentRegistry::from_bytes(&bytes).unwrap();
814 assert_eq!(reg, restored);
815 }
816
817 #[xmtp_common::test]
818 fn test_iter() {
819 let mut reg = ComponentRegistry::new();
820 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
821 reg.set(ComponentId::GROUP_DESCRIPTION, sample_meta())
822 .unwrap();
823
824 let entries: Vec<_> = reg.iter().collect::<Result<Vec<_>, _>>().unwrap();
825 assert_eq!(entries.len(), 2);
826 assert!(entries[0].0 < entries[1].0);
827 }
828
829 #[xmtp_common::test]
830 fn test_empty_registry_round_trip() {
831 let reg = ComponentRegistry::new();
832 let bytes = reg.to_bytes().unwrap();
833 let restored = ComponentRegistry::from_bytes(&bytes).unwrap();
834 assert_eq!(reg, restored);
835 assert!(restored.is_empty());
836 }
837
838 fn assert_tolerated(bytes: &[u8], id: ComponentId) {
842 let reg = ComponentRegistry::from_bytes(bytes).unwrap();
843 assert!(reg.get(&id).unwrap().is_none());
844 assert!(!reg.contains(&id));
845 assert_eq!(reg.len(), 0);
846 assert!(reg.iter().next().is_none());
847 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![id]);
848 assert_eq!(reg.to_bytes().unwrap(), bytes);
850 }
851
852 #[xmtp_common::test]
853 fn test_from_bytes_tolerates_out_of_space_id() {
854 let id = ComponentId::new(0x0001);
858 let bytes = raw_bytes_with_entry(id, sample_meta().encode_to_vec());
859 assert_tolerated(&bytes, id);
860 }
861
862 #[xmtp_common::test]
863 fn test_from_bytes_tolerates_reserved_id() {
864 let id = ComponentId::new(0xFF50);
868 let bytes = raw_bytes_with_entry(id, sample_meta().encode_to_vec());
869 assert_tolerated(&bytes, id);
870 }
871
872 fn raw_bytes_with_entry(id: ComponentId, value: Vec<u8>) -> Vec<u8> {
877 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
878 map.insert(id, VLBytes::new(value)).unwrap();
879 map.tls_serialize_detached().unwrap()
880 }
881
882 #[xmtp_common::test]
883 fn test_from_bytes_tolerates_hardcoded_id() {
884 let bytes = raw_bytes_with_entry(
888 ComponentId::COMPONENT_REGISTRY,
889 sample_meta().encode_to_vec(),
890 );
891 assert_tolerated(&bytes, ComponentId::COMPONENT_REGISTRY);
892
893 let bytes =
894 raw_bytes_with_entry(ComponentId::SUPER_ADMIN_LIST, sample_meta().encode_to_vec());
895 assert_tolerated(&bytes, ComponentId::SUPER_ADMIN_LIST);
896 }
897
898 #[xmtp_common::test]
899 fn test_from_bytes_tolerates_missing_permissions() {
900 let meta = ComponentMetadata {
905 permissions: None,
906 external_committer_permissions: None,
907 component_type: ComponentType::Bytes as i32,
908 };
909 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, meta.encode_to_vec());
910 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
911 }
912
913 #[xmtp_common::test]
914 fn test_from_bytes_tolerates_missing_policy_field() {
915 let meta = new_component_metadata(
917 ComponentPermissions {
918 insert_policy: Some(allow()),
919 update_policy: None,
920 delete_policy: Some(allow()),
921 },
922 ComponentType::Bytes,
923 );
924 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, meta.encode_to_vec());
925 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
926 }
927
928 #[xmtp_common::test]
929 fn test_from_bytes_tolerates_constrained_violation() {
930 let meta = new_component_metadata(
935 component_permissions()
936 .insert(allow())
937 .update(allow())
938 .delete(allow())
939 .call(),
940 ComponentType::Bytes,
941 );
942 let bytes = raw_bytes_with_entry(ComponentId::ADMIN_LIST, meta.encode_to_vec());
943 assert_tolerated(&bytes, ComponentId::ADMIN_LIST);
944 }
945
946 #[xmtp_common::test]
947 fn test_reads_skip_entry_with_undecodable_value() {
948 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
954 map.insert(
955 ComponentId::GROUP_NAME,
956 VLBytes::new(sample_meta().encode_to_vec()),
957 )
958 .unwrap();
959 map.insert(
960 ComponentId::GROUP_DESCRIPTION,
961 VLBytes::new(vec![0xFF, 0xFF, 0xFF, 0xFF]),
962 )
963 .unwrap();
964 let bytes = map.tls_serialize_detached().unwrap();
965
966 let reg = ComponentRegistry::from_bytes(&bytes).unwrap();
967
968 let entries: Vec<_> = reg.iter().collect::<Result<Vec<_>, _>>().unwrap();
969 assert_eq!(entries.len(), 1);
970 assert_eq!(entries[0].0, ComponentId::GROUP_NAME);
971 assert_eq!(
972 reg.get(&ComponentId::GROUP_NAME).unwrap(),
973 Some(sample_meta())
974 );
975 assert!(reg.get(&ComponentId::GROUP_DESCRIPTION).unwrap().is_none());
976 assert_eq!(
977 reg.unrecognized_ids().collect::<Vec<_>>(),
978 vec![ComponentId::GROUP_DESCRIPTION]
979 );
980 assert_eq!(reg.to_bytes().unwrap(), bytes);
981 }
982
983 #[xmtp_common::test]
984 fn test_from_bytes_tolerates_malformed_protobuf_value() {
985 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, vec![0xFF, 0xFF, 0xFF, 0xFF]);
989 assert_tolerated(&bytes, ComponentId::GROUP_NAME);
990 }
991
992 #[xmtp_common::test]
993 fn test_from_bytes_rejects_undecodable_outer_map() {
994 let mut bytes =
997 raw_bytes_with_entry(ComponentId::GROUP_NAME, sample_meta().encode_to_vec());
998 bytes.truncate(bytes.len() - 1);
999 assert!(matches!(
1000 ComponentRegistry::from_bytes(&bytes),
1001 Err(ComponentRegistryError::TlsCodecError(_))
1002 ));
1003 }
1004
1005 #[xmtp_common::test]
1006 fn test_from_bytes_mixed_valid_and_invalid_entries() {
1007 let reserved = ComponentId::new(0xFF00);
1011 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
1012 map.insert(
1013 ComponentId::GROUP_NAME,
1014 VLBytes::new(sample_meta().encode_to_vec()),
1015 )
1016 .unwrap();
1017 map.insert(reserved, VLBytes::new(b"future-slot".to_vec()))
1018 .unwrap();
1019 let bytes = map.tls_serialize_detached().unwrap();
1020
1021 let reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1022 assert_eq!(reg.len(), 1);
1023 assert_eq!(
1024 reg.get(&ComponentId::GROUP_NAME).unwrap(),
1025 Some(sample_meta())
1026 );
1027 assert!(!reg.contains(&reserved));
1028 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![reserved]);
1029 assert_eq!(reg.to_bytes().unwrap(), bytes);
1030 }
1031
1032 #[xmtp_common::test(unwrap_try = true)]
1049 fn test_poisoned_registry_still_validates_unrelated_writes() {
1050 use crate::app_data::validation::{
1051 ActorAuthority, ComponentChange, ComponentPermissionError, validate_component_write,
1052 };
1053
1054 let poisoned = ComponentId::new(0xFF20);
1059 let mut map: TlsMap<ComponentId, VLBytes> = TlsMap::new();
1060 map.insert(
1061 ComponentId::GROUP_NAME,
1062 VLBytes::new(sample_meta().encode_to_vec()),
1063 )?;
1064 map.insert(poisoned, VLBytes::new(vec![0xDE, 0xAD, 0xBE, 0xEF]))?;
1065 let bytes = map.tls_serialize_detached()?;
1066
1067 let reg = ComponentRegistry::from_bytes(&bytes)?;
1069 assert_eq!(reg.unrecognized_ids().collect::<Vec<_>>(), vec![poisoned]);
1070
1071 assert_eq!(reg.get(&ComponentId::GROUP_NAME)?, Some(sample_meta()));
1074 ComponentRegistry::validate_entry(ComponentId::GROUP_NAME, &sample_meta().encode_to_vec())?;
1075
1076 let member = ActorAuthority {
1079 is_admin: false,
1080 is_super_admin: false,
1081 };
1082 let healthy_write = ComponentChange::builder()
1083 .component_id(ComponentId::GROUP_NAME)
1084 .op(ComponentOp::Insert)
1085 .actor(member)
1086 .build();
1087 validate_component_write(&healthy_write, ®)?;
1088
1089 let poisoned_write = ComponentChange::builder()
1092 .component_id(poisoned)
1093 .op(ComponentOp::Insert)
1094 .actor(member)
1095 .build();
1096 assert!(matches!(
1097 validate_component_write(&poisoned_write, ®),
1098 Err(ComponentPermissionError::NoRegistryEntry(id)) if id == poisoned
1099 ));
1100
1101 assert_eq!(reg.to_bytes()?, bytes);
1104 }
1105
1106 #[xmtp_common::test]
1107 fn test_set_repairs_unrecognized_entry() {
1108 let broken = ComponentMetadata {
1112 permissions: None,
1113 external_committer_permissions: None,
1114 component_type: ComponentType::Bytes as i32,
1115 };
1116 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, broken.encode_to_vec());
1117 let mut reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1118 assert_eq!(reg.unrecognized_ids().count(), 1);
1119
1120 reg.set(ComponentId::GROUP_NAME, sample_meta()).unwrap();
1121 assert_eq!(reg.unrecognized_ids().count(), 0);
1122 assert_eq!(
1123 reg.get(&ComponentId::GROUP_NAME).unwrap(),
1124 Some(sample_meta())
1125 );
1126
1127 let round_tripped = ComponentRegistry::from_bytes(®.to_bytes().unwrap()).unwrap();
1128 assert_eq!(round_tripped.unrecognized_ids().count(), 0);
1129 assert_eq!(
1130 round_tripped.get(&ComponentId::GROUP_NAME).unwrap(),
1131 Some(sample_meta())
1132 );
1133 }
1134
1135 #[xmtp_common::test]
1136 fn test_remove_deletes_unrecognized_entry() {
1137 let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, vec![0xFF, 0xFF]);
1140 let mut reg = ComponentRegistry::from_bytes(&bytes).unwrap();
1141 assert_eq!(reg.unrecognized_ids().count(), 1);
1142
1143 reg.remove(&ComponentId::GROUP_NAME).unwrap();
1144 assert_eq!(reg.unrecognized_ids().count(), 0);
1145 let empty = ComponentRegistry::new();
1146 assert_eq!(reg.to_bytes().unwrap(), empty.to_bytes().unwrap());
1147 }
1148
1149 #[xmtp_common::test]
1150 fn test_preserves_component_type() {
1151 let mut reg = ComponentRegistry::new();
1152 let meta = new_component_metadata(
1153 component_permissions()
1154 .insert(allow())
1155 .update(allow())
1156 .delete(deny())
1157 .call(),
1158 ComponentType::TlsMapBytesBytes,
1159 );
1160 reg.set(ComponentId::GROUP_MEMBERSHIP, meta).unwrap();
1161
1162 let got = reg.get(&ComponentId::GROUP_MEMBERSHIP).unwrap().unwrap();
1163 assert_eq!(got.component_type, ComponentType::TlsMapBytesBytes as i32);
1164 }
1165}