1#![expect(dead_code)]
33
34use openmls::{
35 extensions::Extensions,
36 group::{GroupContext, MlsGroup as OpenMlsGroup, StagedCommit},
37 messages::proposals::AppDataUpdateOperation,
38};
39use tls_codec::{Deserialize, Serialize};
40use xmtp_mls_common::{
41 app_data::{
42 component_id::ComponentId,
43 component_registry::ComponentRegistry,
44 components::type_dispatch::{apply_update_payload_for_type, expand_to_changes_for_type},
45 registry_table::lookup_component,
46 typed::ComponentTypedError,
47 },
48 group_mutable_metadata::{
49 GroupMutableMetadata, GroupMutableMetadataError, MetadataField,
50 find_mutable_metadata_extension,
51 },
52 inbox_id::{InboxId, InboxIdError},
53 tls_map::TlsMapError,
54 tls_set::{TlsSet, TlsSetDelta, TlsSetError, TlsSetMutation},
55};
56use xmtp_proto::xmtp::mls::message_contents::ComponentType;
57
58#[derive(Debug, thiserror::Error)]
66pub enum ComponentSourceError {
67 #[error("unknown component {0}")]
69 UnknownComponent(ComponentId),
70
71 #[error("component {0} wiring is not yet implemented")]
73 NotImplemented(ComponentId),
74
75 #[error("component {0} is immutable and cannot be updated via AppDataUpdate")]
79 ImmutableUpdate(ComponentId),
80
81 #[error("mutation shape does not match component {0}")]
85 MismatchedMutation(ComponentId),
86
87 #[error("invalid inbox id: {0}")]
92 InvalidInboxId(#[from] InboxIdError),
93
94 #[error("malformed value for component {component_id}: {reason}")]
99 MalformedComponentValue {
100 component_id: ComponentId,
102 reason: String,
104 },
105
106 #[error("unknown metadata field name: {0}")]
111 UnknownMetadataField(String),
112
113 #[error(transparent)]
116 GroupMutableMetadata(#[from] GroupMutableMetadataError),
117
118 #[error("tls codec error: {0}")]
120 TlsCodec(#[from] tls_codec::Error),
121
122 #[error("tls set apply error: {0}")]
125 TlsSetApply(#[from] TlsSetError),
126
127 #[error("tls map apply error: {0}")]
130 TlsMapApply(#[from] TlsMapError),
131}
132
133impl ComponentSourceError {
134 pub(crate) fn component_id(&self) -> Option<ComponentId> {
140 match self {
141 Self::UnknownComponent(id)
142 | Self::NotImplemented(id)
143 | Self::ImmutableUpdate(id)
144 | Self::MismatchedMutation(id)
145 | Self::MalformedComponentValue {
146 component_id: id, ..
147 } => Some(*id),
148 _ => None,
149 }
150 }
151}
152
153impl From<ComponentTypedError> for ComponentSourceError {
154 fn from(err: ComponentTypedError) -> Self {
160 match err {
161 ComponentTypedError::ImmutableUpdate(id) => Self::ImmutableUpdate(id),
162 ComponentTypedError::MismatchedMutation(id) => Self::MismatchedMutation(id),
163 ComponentTypedError::MalformedValue {
164 component_id,
165 reason,
166 } => Self::MalformedComponentValue {
167 component_id,
168 reason,
169 },
170 ComponentTypedError::InvalidInboxId(e) => Self::InvalidInboxId(e),
171 ComponentTypedError::TlsCodec(e) => Self::TlsCodec(e),
172 ComponentTypedError::TlsSetApply(e) => Self::TlsSetApply(e),
173 ComponentTypedError::TlsMapApply(e) => Self::TlsMapApply(e),
174 ComponentTypedError::UnspecifiedType(id) => Self::MalformedComponentValue {
175 component_id: id,
176 reason: "registered ComponentType is Unspecified".to_string(),
177 },
178 ComponentTypedError::RegistryMutation(e) => Self::MalformedComponentValue {
184 component_id: ComponentId::COMPONENT_REGISTRY,
185 reason: e.to_string(),
186 },
187 }
188 }
189}
190
191impl From<ComponentSourceError> for GroupMutableMetadataError {
192 fn from(err: ComponentSourceError) -> Self {
205 if let ComponentSourceError::GroupMutableMetadata(inner) = err {
206 return inner;
207 }
208 let component_id = err.component_id();
209 GroupMutableMetadataError::MalformedComponent {
210 component_id,
211 reason: err.to_string(),
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
230pub(crate) enum ComponentMutation<'a> {
231 Bytes {
233 component_id: ComponentId,
234 new_value: &'a [u8],
235 },
236 AdminListAdd { inbox_id: &'a str },
238 AdminListRemove { inbox_id: &'a str },
240 SuperAdminListAdd { inbox_id: &'a str },
242 SuperAdminListRemove { inbox_id: &'a str },
244}
245
246impl ComponentMutation<'_> {
247 pub(crate) fn component_id(&self) -> ComponentId {
249 match self {
250 Self::Bytes { component_id, .. } => *component_id,
251 Self::AdminListAdd { .. } | Self::AdminListRemove { .. } => ComponentId::ADMIN_LIST,
252 Self::SuperAdminListAdd { .. } | Self::SuperAdminListRemove { .. } => {
253 ComponentId::SUPER_ADMIN_LIST
254 }
255 }
256 }
257}
258
259pub(crate) fn component_type(id: ComponentId) -> Option<ComponentType> {
263 match id {
264 ComponentId::COMPONENT_REGISTRY => Some(ComponentType::TlsMapBytesBytes),
268 ComponentId::SUPER_ADMIN_LIST => Some(ComponentType::TlsSetInboxId),
269 ComponentId::ADMIN_LIST => Some(ComponentType::TlsSetInboxId),
270
271 ComponentId::GROUP_MEMBERSHIP => Some(ComponentType::TlsMapInboxIdBytes),
273
274 ComponentId::GROUP_NAME
276 | ComponentId::GROUP_DESCRIPTION
277 | ComponentId::GROUP_IMAGE_URL
278 | ComponentId::APP_DATA
279 | ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION => Some(ComponentType::String),
280
281 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
283 | ComponentId::MESSAGE_DISAPPEAR_IN_NS
284 | ComponentId::COMMIT_LOG_SIGNER => Some(ComponentType::Bytes),
285
286 ComponentId::CONVERSATION_TYPE
289 | ComponentId::CREATOR_INBOX_ID
290 | ComponentId::ONESHOT_MESSAGE => Some(ComponentType::Bytes),
291 ComponentId::DM_MEMBERS => Some(ComponentType::TlsSetInboxId),
292
293 _ => None,
294 }
295}
296
297pub(crate) use xmtp_mls_common::group_mutable_metadata::METADATA_FIELD_COMPONENT_MAP;
301
302pub(crate) fn metadata_field_to_component_id(field_name: &str) -> Option<ComponentId> {
307 METADATA_FIELD_COMPONENT_MAP
308 .iter()
309 .find(|(field, _)| field.as_str() == field_name)
310 .map(|(_, id)| *id)
311}
312
313pub(crate) fn component_id_to_metadata_field(id: ComponentId) -> Option<MetadataField> {
320 METADATA_FIELD_COMPONENT_MAP
321 .iter()
322 .find(|(_, component_id)| *component_id == id)
323 .map(|(field, _)| *field)
324}
325
326pub(crate) fn read_component_bytes(
331 id: ComponentId,
332 extensions: &Extensions<GroupContext>,
333 proposals_enabled: bool,
334) -> Result<Option<Vec<u8>>, ComponentSourceError> {
335 if proposals_enabled {
336 Ok(read_from_app_data_dict_from_extensions(id, extensions))
337 } else {
338 read_from_legacy(id, extensions)
339 }
340}
341
342pub(crate) fn read_post_commit_component_bytes(
386 id: ComponentId,
387 mls_group: &OpenMlsGroup,
388 staged_commit: &StagedCommit,
389 registry: &ComponentRegistry,
390) -> Result<Option<Vec<u8>>, ComponentSourceError> {
391 let openmls_id: openmls::component::ComponentId = id.as_u16();
392
393 enum Op {
401 Update(Vec<u8>),
402 Remove,
403 }
404 let ops: Vec<Op> = staged_commit
405 .app_data_update_proposals()
406 .filter_map(|queued| {
407 let proposal = queued.app_data_update_proposal();
408 if proposal.component_id() != openmls_id {
409 return None;
410 }
411 Some(match proposal.operation() {
412 AppDataUpdateOperation::Update(payload) => Op::Update(payload.as_slice().to_vec()),
413 AppDataUpdateOperation::Remove => Op::Remove,
414 })
415 })
416 .collect();
417 if ops.is_empty() {
418 return Ok(read_from_app_data_dict(id, mls_group));
419 }
420
421 let mut current = read_from_app_data_dict(id, mls_group);
422 for op in &ops {
423 match op {
424 Op::Update(payload) => {
425 current = Some(apply_app_data_update_payload(
426 id,
427 payload,
428 current.as_deref(),
429 registry,
430 )?);
431 }
432 Op::Remove => current = None,
433 }
434 }
435 Ok(current)
436}
437
438pub(crate) fn read_from_app_data_dict(
448 id: ComponentId,
449 mls_group: &OpenMlsGroup,
450) -> Option<Vec<u8>> {
451 read_from_app_data_dict_from_extensions(id, mls_group.extensions())
452}
453
454pub(crate) fn read_from_app_data_dict_from_extensions(
459 id: ComponentId,
460 extensions: &Extensions<GroupContext>,
461) -> Option<Vec<u8>> {
462 extensions
463 .app_data_dictionary()
464 .and_then(|ext| ext.dictionary().get(&id.as_u16()))
465 .map(|bytes| bytes.to_vec())
466}
467
468fn read_from_legacy(
484 id: ComponentId,
485 extensions: &Extensions<GroupContext>,
486) -> Result<Option<Vec<u8>>, ComponentSourceError> {
487 if let Some(field) = component_id_to_metadata_field(id) {
490 let gmm = match find_mutable_metadata_extension(extensions) {
491 Some(bytes) => GroupMutableMetadata::try_from(bytes)?,
492 None => return Ok(None),
493 };
494 return Ok(gmm
495 .attributes
496 .get(field.as_str())
497 .map(|s| s.as_bytes().to_vec()));
498 }
499
500 match id {
501 ComponentId::ADMIN_LIST => {
502 let gmm = match find_mutable_metadata_extension(extensions) {
503 Some(bytes) => GroupMutableMetadata::try_from(bytes)?,
504 None => return Ok(None),
505 };
506 Ok(Some(encode_inbox_id_set(&gmm.admin_list)?))
507 }
508 ComponentId::SUPER_ADMIN_LIST => {
509 let gmm = match find_mutable_metadata_extension(extensions) {
510 Some(bytes) => GroupMutableMetadata::try_from(bytes)?,
511 None => return Ok(None),
512 };
513 Ok(Some(encode_inbox_id_set(&gmm.super_admin_list)?))
514 }
515 ComponentId::GROUP_MEMBERSHIP => Err(ComponentSourceError::NotImplemented(id)),
516 _ => Err(ComponentSourceError::UnknownComponent(id)),
517 }
518}
519
520pub(crate) fn encode_app_data_update_payload(
527 mutation: &ComponentMutation<'_>,
528) -> Result<Vec<u8>, ComponentSourceError> {
529 match mutation {
530 ComponentMutation::Bytes {
531 component_id,
532 new_value,
533 } => {
534 if component_id_to_metadata_field(*component_id).is_none() {
536 return Err(ComponentSourceError::MismatchedMutation(*component_id));
537 }
538 Ok(new_value.to_vec())
539 }
540 ComponentMutation::AdminListAdd { inbox_id }
541 | ComponentMutation::SuperAdminListAdd { inbox_id } => {
542 let key = inbox_id_str_to_bytes(inbox_id)?;
543 encode_inbox_id_set_delta(TlsSetMutation::Insert(key))
544 }
545 ComponentMutation::AdminListRemove { inbox_id }
546 | ComponentMutation::SuperAdminListRemove { inbox_id } => {
547 let key = inbox_id_str_to_bytes(inbox_id)?;
548 encode_inbox_id_set_delta(TlsSetMutation::Remove(key))
549 }
550 }
551}
552
553pub(crate) use xmtp_mls_common::app_data::typed::ExpandedComponentChange;
558
559pub(crate) fn expand_app_data_update_to_changes(
588 component_id: ComponentId,
589 operation: &AppDataUpdateOperation,
590 old_value: Option<&[u8]>,
591 registry: &ComponentRegistry,
592) -> Result<Vec<ExpandedComponentChange>, ComponentSourceError> {
593 if let Some(component) = lookup_component(component_id) {
594 return component
595 .expand_to_changes(operation, old_value)
596 .map_err(Into::into);
597 }
598
599 let ty = component_type(component_id)
616 .map_or_else(|| registered_component_type(component_id, registry), Ok)?;
617 expand_to_changes_for_type(component_id, ty, operation, old_value).map_err(Into::into)
618}
619
620pub(crate) fn apply_app_data_update_payload(
632 id: ComponentId,
633 payload: &[u8],
634 old_value: Option<&[u8]>,
635 registry: &ComponentRegistry,
636) -> Result<Vec<u8>, ComponentSourceError> {
637 if id.is_immutable() && old_value.is_some() {
644 return Err(ComponentSourceError::ImmutableUpdate(id));
645 }
646
647 if let Some(component) = lookup_component(id) {
650 return component
651 .apply_update_payload(payload, old_value)
652 .map_err(Into::into);
653 }
654
655 let ty = component_type(id).map_or_else(|| registered_component_type(id, registry), Ok)?;
666 apply_update_payload_for_type(id, ty, payload, old_value).map_err(Into::into)
667}
668
669fn registered_component_type(
675 id: ComponentId,
676 registry: &ComponentRegistry,
677) -> Result<ComponentType, ComponentSourceError> {
678 let meta = registry
679 .get(&id)
680 .map_err(|e| ComponentSourceError::MalformedComponentValue {
681 component_id: id,
682 reason: format!("registry lookup: {e}"),
683 })?
684 .ok_or(ComponentSourceError::UnknownComponent(id))?;
685 ComponentType::try_from(meta.component_type).map_err(|_| {
686 ComponentSourceError::MalformedComponentValue {
687 component_id: id,
688 reason: format!(
689 "registry entry has unknown component_type tag {}",
690 meta.component_type
691 ),
692 }
693 })
694}
695
696pub(crate) fn merge_app_data_into_mutable_metadata(
727 base: &mut GroupMutableMetadata,
728 mls_group: &OpenMlsGroup,
729) -> Result<(), ComponentSourceError> {
730 merge_app_data_into_mutable_metadata_from_extensions(base, mls_group.extensions())
731}
732
733pub(crate) fn extract_group_mutable_metadata_capability_aware(
753 mls_group: &OpenMlsGroup,
754) -> Result<GroupMutableMetadata, ComponentSourceError> {
755 if super::is_migrated_group(mls_group) {
756 let mut base =
757 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new());
758 merge_app_data_into_mutable_metadata(&mut base, mls_group)?;
759 Ok(base)
760 } else {
761 Ok(GroupMutableMetadata::try_from(mls_group)?)
762 }
763}
764
765pub(crate) fn extract_group_mutable_metadata_capability_aware_from_extensions(
771 extensions: &Extensions<GroupContext>,
772) -> Result<GroupMutableMetadata, ComponentSourceError> {
773 if super::is_migrated_extensions(extensions) {
774 let mut base =
775 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new());
776 merge_app_data_into_mutable_metadata_from_extensions(&mut base, extensions)?;
777 Ok(base)
778 } else {
779 Ok(GroupMutableMetadata::try_from(extensions)?)
780 }
781}
782
783pub(crate) fn merge_app_data_into_mutable_metadata_from_extensions(
790 base: &mut GroupMutableMetadata,
791 extensions: &openmls::extensions::Extensions<openmls::group::GroupContext>,
792) -> Result<(), ComponentSourceError> {
793 if !super::is_migrated_extensions(extensions) {
794 return Ok(());
795 }
796 xmtp_mls_common::group_mutable_metadata::merge_dict_into_mutable_metadata(base, extensions)
804 .map_err(|e| match e {
805 GroupMutableMetadataError::MalformedComponent {
806 component_id: Some(component_id),
807 reason,
808 } => ComponentSourceError::MalformedComponentValue {
809 component_id,
810 reason,
811 },
812 other => ComponentSourceError::GroupMutableMetadata(other),
813 })
814}
815
816pub(crate) fn inbox_id_str_to_bytes(inbox_id: &str) -> Result<InboxId, ComponentSourceError> {
834 InboxId::from_hex(inbox_id).map_err(Into::into)
835}
836
837pub(crate) fn read_super_admin_list_from_dict(
845 mls_group: &OpenMlsGroup,
846) -> Result<Option<Vec<String>>, ComponentSourceError> {
847 read_super_admin_list_from_extensions(mls_group.extensions())
848}
849
850pub(crate) fn read_super_admin_list_from_extensions(
855 extensions: &Extensions<GroupContext>,
856) -> Result<Option<Vec<String>>, ComponentSourceError> {
857 if !super::is_migrated_extensions(extensions) {
858 return Ok(None);
859 }
860 let Some(ext) = extensions.app_data_dictionary() else {
861 return Ok(None);
862 };
863 let Some(bytes) = ext
864 .dictionary()
865 .get(&ComponentId::SUPER_ADMIN_LIST.as_u16())
866 else {
867 return Ok(None);
868 };
869 let set = TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(|e| {
870 ComponentSourceError::MalformedComponentValue {
871 component_id: ComponentId::SUPER_ADMIN_LIST,
872 reason: format!("invalid TlsSet<InboxId>: {e}"),
873 }
874 })?;
875 Ok(Some(set.iter().map(|id| id.to_hex()).collect()))
876}
877
878pub(crate) fn read_group_metadata_from_dict(
898 mls_group: &OpenMlsGroup,
899) -> Result<Option<GroupMetadataReturn>, ComponentSourceError> {
900 read_group_metadata_from_extensions(mls_group.extensions())
901}
902
903pub(crate) fn read_group_metadata_from_extensions(
906 extensions: &Extensions<GroupContext>,
907) -> Result<Option<GroupMetadataReturn>, ComponentSourceError> {
908 use prost::Message;
909 use xmtp_proto::xmtp::mls::message_contents::{
910 DmMembers as DmMembersProto, Inbox as InboxProto, OneshotMessage,
911 };
912
913 if !super::is_migrated_extensions(extensions) {
916 return Ok(None);
917 }
918
919 let Some(ext) = extensions.app_data_dictionary() else {
920 return Ok(None);
921 };
922 let dict = ext.dictionary();
923
924 let Some(ct_bytes) = dict.get(&ComponentId::CONVERSATION_TYPE.as_u16()) else {
925 return Ok(None);
926 };
927 let Some(creator_bytes) = dict.get(&ComponentId::CREATOR_INBOX_ID.as_u16()) else {
928 return Ok(None);
929 };
930
931 let ct_arr: [u8; 4] =
932 ct_bytes
933 .try_into()
934 .map_err(|_| ComponentSourceError::MalformedComponentValue {
935 component_id: ComponentId::CONVERSATION_TYPE,
936 reason: format!("expected 4 bytes, got {}", ct_bytes.len()),
937 })?;
938 let conversation_type = i32::from_be_bytes(ct_arr);
939
940 let creator_inbox_id = InboxId::tls_deserialize_exact(creator_bytes)
941 .map_err(|e| ComponentSourceError::MalformedComponentValue {
942 component_id: ComponentId::CREATOR_INBOX_ID,
943 reason: format!("invalid InboxId TLS encoding: {e}"),
944 })?
945 .to_hex();
946
947 let dm_members = match dict.get(&ComponentId::DM_MEMBERS.as_u16()) {
950 Some(b) => {
951 let set = TlsSet::<InboxId>::tls_deserialize_exact(b).map_err(|e| {
952 ComponentSourceError::MalformedComponentValue {
953 component_id: ComponentId::DM_MEMBERS,
954 reason: format!("invalid TlsSet<InboxId>: {e}"),
955 }
956 })?;
957 let ids: Vec<InboxId> = set.iter().copied().collect();
958 if ids.len() != 2 {
959 return Err(ComponentSourceError::MalformedComponentValue {
960 component_id: ComponentId::DM_MEMBERS,
961 reason: format!("expected 2 inbox ids, got {}", ids.len()),
962 });
963 }
964 Some(DmMembersProto {
965 dm_member_one: Some(InboxProto {
966 inbox_id: ids[0].to_hex(),
967 }),
968 dm_member_two: Some(InboxProto {
969 inbox_id: ids[1].to_hex(),
970 }),
971 })
972 }
973 None => None,
974 };
975
976 let oneshot = match dict.get(&ComponentId::ONESHOT_MESSAGE.as_u16()) {
977 Some(b) => Some(OneshotMessage::decode(b).map_err(|e| {
978 ComponentSourceError::MalformedComponentValue {
979 component_id: ComponentId::ONESHOT_MESSAGE,
980 reason: format!("OneshotMessage prost decode: {e}"),
981 }
982 })?),
983 None => None,
984 };
985
986 Ok(Some(GroupMetadataReturn {
987 conversation_type,
988 creator_inbox_id,
989 dm_members,
990 oneshot,
991 }))
992}
993
994#[derive(Debug)]
997pub(crate) struct GroupMetadataReturn {
998 pub conversation_type: i32,
999 pub creator_inbox_id: String,
1000 pub dm_members: Option<xmtp_proto::xmtp::mls::message_contents::DmMembers>,
1001 pub oneshot: Option<xmtp_proto::xmtp::mls::message_contents::OneshotMessage>,
1002}
1003
1004pub(crate) fn read_group_membership_from_dict(
1011 extensions: &Extensions<GroupContext>,
1012) -> Result<Option<xmtp_proto::xmtp::mls::message_contents::GroupMembership>, ComponentSourceError>
1013{
1014 use xmtp_mls_common::app_data::migration::decode_group_membership_dict;
1015 use xmtp_proto::xmtp::mls::message_contents::GroupMembership as GroupMembershipProto;
1016
1017 if !super::is_migrated_extensions(extensions) {
1023 return Ok(None);
1024 }
1025
1026 let Some(ext) = extensions.app_data_dictionary() else {
1027 return Ok(None);
1028 };
1029 let Some(bytes) = ext
1030 .dictionary()
1031 .get(&ComponentId::GROUP_MEMBERSHIP.as_u16())
1032 else {
1033 return Ok(None);
1034 };
1035
1036 let entries = decode_group_membership_dict(bytes).map_err(|e| {
1037 ComponentSourceError::MalformedComponentValue {
1038 component_id: ComponentId::GROUP_MEMBERSHIP,
1039 reason: format!("TlsMap decode: {e}"),
1040 }
1041 })?;
1042
1043 use xmtp_proto::xmtp::mls::message_contents::group_membership_entry::Version as GroupMembershipEntryVersion;
1055 let mut members: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
1056 let mut failed: Vec<Vec<u8>> = Vec::new();
1057 for (inbox_id, entry) in entries {
1058 let v1 = match entry.version {
1059 Some(GroupMembershipEntryVersion::V1(v1)) => v1,
1060 None => {
1061 return Err(ComponentSourceError::MalformedComponentValue {
1062 component_id: ComponentId::GROUP_MEMBERSHIP,
1063 reason: format!(
1064 "GroupMembershipEntry for {} has no version",
1065 inbox_id.to_hex()
1066 ),
1067 });
1068 }
1069 };
1070 members.insert(inbox_id.to_hex(), v1.sequence_id);
1071 failed.extend(v1.failed_installations);
1072 }
1073
1074 Ok(Some(GroupMembershipProto {
1075 members,
1076 failed_installations: failed,
1077 }))
1078}
1079
1080fn encode_inbox_id_set(inbox_ids: &[String]) -> Result<Vec<u8>, ComponentSourceError> {
1082 let ids: Vec<InboxId> = inbox_ids
1083 .iter()
1084 .map(|s| inbox_id_str_to_bytes(s))
1085 .collect::<Result<Vec<_>, _>>()?;
1086 let set: TlsSet<InboxId> = ids.into_iter().collect();
1087 Ok(set.tls_serialize_detached()?)
1088}
1089
1090fn encode_inbox_id_set_delta(
1092 mutation: TlsSetMutation<InboxId>,
1093) -> Result<Vec<u8>, ComponentSourceError> {
1094 let delta = TlsSetDelta::<InboxId> {
1095 mutations: vec![mutation],
1096 };
1097 Ok(delta.tls_serialize_detached()?)
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103 use prost::Message;
1104 use tls_codec::VLBytes;
1105 use xmtp_mls_common::{
1106 app_data::{
1107 component_permissions::component_permissions,
1108 component_registry::{ComponentOp, new_component_metadata},
1109 },
1110 inbox_id::INBOX_ID_BYTE_LEN,
1111 tls_map::{TlsMap, TlsMapDelta},
1112 tls_set::TlsKeyHash,
1113 };
1114 use xmtp_proto::xmtp::mls::message_contents::{
1115 MetadataPolicy as MetadataPolicyProto,
1116 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
1117 };
1118
1119 fn fake_inbox_id(tag: u8) -> String {
1123 hex::encode([tag; INBOX_ID_BYTE_LEN])
1124 }
1125
1126 fn fake_inbox(tag: u8) -> InboxId {
1128 InboxId::from_bytes([tag; INBOX_ID_BYTE_LEN])
1129 }
1130
1131 fn empty_registry() -> ComponentRegistry {
1135 ComponentRegistry::new()
1136 }
1137
1138 fn registry_with(id: ComponentId, ty: ComponentType) -> ComponentRegistry {
1143 fn allow() -> MetadataPolicyProto {
1144 MetadataPolicyProto {
1145 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Allow as i32)),
1146 }
1147 }
1148 let perms = component_permissions()
1149 .insert(allow())
1150 .update(allow())
1151 .delete(allow())
1152 .call();
1153 let meta = new_component_metadata(perms, ty);
1154 let mut reg = ComponentRegistry::new();
1155 reg.set(id, meta).unwrap();
1156 reg
1157 }
1158
1159 #[xmtp_common::test]
1162 fn test_inbox_id_round_trip() {
1163 let original = fake_inbox_id(0xAB);
1164 let id = inbox_id_str_to_bytes(&original).unwrap();
1165 assert_eq!(id.as_bytes(), &[0xAB; 32]);
1166 assert_eq!(id.to_hex(), original);
1167 }
1168
1169 #[xmtp_common::test]
1170 fn test_inbox_id_invalid_hex() {
1171 let err = inbox_id_str_to_bytes("not_hex").unwrap_err();
1172 assert!(
1173 matches!(
1174 err,
1175 ComponentSourceError::InvalidInboxId(InboxIdError::InvalidHex(_))
1176 ),
1177 "got {err:?}"
1178 );
1179 }
1180
1181 #[xmtp_common::test]
1182 fn test_inbox_id_wrong_length() {
1183 let err = inbox_id_str_to_bytes(&"ab".repeat(16)).unwrap_err();
1185 assert!(
1186 matches!(
1187 err,
1188 ComponentSourceError::InvalidInboxId(InboxIdError::InvalidLength {
1189 expected: INBOX_ID_BYTE_LEN,
1190 actual: 16,
1191 })
1192 ),
1193 "got {err:?}"
1194 );
1195 }
1196
1197 #[xmtp_common::test]
1200 fn test_component_type_string_family() {
1201 for id in [
1204 ComponentId::GROUP_NAME,
1205 ComponentId::GROUP_DESCRIPTION,
1206 ComponentId::GROUP_IMAGE_URL,
1207 ComponentId::APP_DATA,
1208 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
1209 ] {
1210 assert_eq!(component_type(id), Some(ComponentType::String));
1211 }
1212 }
1213
1214 #[xmtp_common::test]
1215 fn test_component_type_bytes_family() {
1216 for id in [
1219 ComponentId::MESSAGE_DISAPPEAR_FROM_NS,
1220 ComponentId::MESSAGE_DISAPPEAR_IN_NS,
1221 ComponentId::COMMIT_LOG_SIGNER,
1222 ] {
1223 assert_eq!(component_type(id), Some(ComponentType::Bytes));
1224 }
1225 }
1226
1227 #[xmtp_common::test]
1228 fn test_component_type_set_inbox_id_family() {
1229 for id in [
1230 ComponentId::ADMIN_LIST,
1231 ComponentId::SUPER_ADMIN_LIST,
1232 ComponentId::DM_MEMBERS,
1233 ] {
1234 assert_eq!(component_type(id), Some(ComponentType::TlsSetInboxId));
1235 }
1236 }
1237
1238 #[xmtp_common::test]
1239 fn test_component_type_group_membership() {
1240 assert_eq!(
1241 component_type(ComponentId::GROUP_MEMBERSHIP),
1242 Some(ComponentType::TlsMapInboxIdBytes)
1243 );
1244 }
1245
1246 #[xmtp_common::test]
1247 fn test_component_type_app_range_is_none() {
1248 assert_eq!(component_type(ComponentId::new(0xC000)), None);
1249 assert_eq!(component_type(ComponentId::new(0xFDAB)), None);
1250 }
1251
1252 #[xmtp_common::test]
1255 fn test_metadata_field_round_trip() {
1256 for field in [
1257 MetadataField::GroupName,
1258 MetadataField::Description,
1259 MetadataField::GroupImageUrlSquare,
1260 MetadataField::MessageDisappearFromNS,
1261 MetadataField::MessageDisappearInNS,
1262 MetadataField::MinimumSupportedProtocolVersion,
1263 MetadataField::CommitLogSigner,
1264 MetadataField::AppData,
1265 ] {
1266 let id = metadata_field_to_component_id(field.as_str())
1267 .expect("every MetadataField has a ComponentId");
1268 let back = component_id_to_metadata_field(id)
1269 .expect("every mapped ComponentId has a MetadataField");
1270 assert_eq!(back, field, "round-trip mismatch for {field:?}");
1271 }
1272 }
1273
1274 #[xmtp_common::test]
1275 fn test_metadata_field_unknown_name_returns_none() {
1276 assert!(metadata_field_to_component_id("nonexistent_field").is_none());
1277 }
1278
1279 #[xmtp_common::test]
1280 fn test_component_id_to_metadata_field_non_gmm_returns_none() {
1281 assert!(component_id_to_metadata_field(ComponentId::ADMIN_LIST).is_none());
1282 assert!(component_id_to_metadata_field(ComponentId::GROUP_MEMBERSHIP).is_none());
1283 assert!(component_id_to_metadata_field(ComponentId::new(0xC000)).is_none());
1284 }
1285
1286 #[xmtp_common::test]
1289 fn test_encode_bytes_payload_passthrough() {
1290 let value = b"hello world";
1291 let payload = encode_app_data_update_payload(&ComponentMutation::Bytes {
1292 component_id: ComponentId::GROUP_NAME,
1293 new_value: value,
1294 })
1295 .unwrap();
1296 assert_eq!(payload, value);
1297 }
1298
1299 #[xmtp_common::test]
1300 fn test_encode_bytes_payload_rejects_non_bytes_component() {
1301 let err = encode_app_data_update_payload(&ComponentMutation::Bytes {
1305 component_id: ComponentId::GROUP_MEMBERSHIP,
1306 new_value: b"x",
1307 })
1308 .unwrap_err();
1309 assert!(matches!(err, ComponentSourceError::MismatchedMutation(_)));
1310 }
1311
1312 #[xmtp_common::test]
1313 fn test_encode_admin_list_insert_delta() {
1314 let inbox = fake_inbox_id(0x11);
1315 let payload =
1316 encode_app_data_update_payload(&ComponentMutation::AdminListAdd { inbox_id: &inbox })
1317 .unwrap();
1318 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&payload).unwrap();
1319 assert_eq!(delta.mutations.len(), 1);
1320 match &delta.mutations[0] {
1321 TlsSetMutation::Insert(k) => assert_eq!(*k, fake_inbox(0x11)),
1322 other => panic!("expected Insert, got {other:?}"),
1323 }
1324 }
1325
1326 #[xmtp_common::test]
1327 fn test_encode_admin_list_remove_delta() {
1328 let inbox = fake_inbox_id(0x22);
1329 let payload = encode_app_data_update_payload(&ComponentMutation::AdminListRemove {
1330 inbox_id: &inbox,
1331 })
1332 .unwrap();
1333 let delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&payload).unwrap();
1334 assert_eq!(delta.mutations.len(), 1);
1335 match &delta.mutations[0] {
1336 TlsSetMutation::Remove(k) => assert_eq!(*k, fake_inbox(0x22)),
1337 other => panic!("expected Remove, got {other:?}"),
1338 }
1339 }
1340
1341 #[xmtp_common::test]
1342 fn test_encode_super_admin_list_delta() {
1343 let inbox = fake_inbox_id(0x33);
1344 let add = encode_app_data_update_payload(&ComponentMutation::SuperAdminListAdd {
1345 inbox_id: &inbox,
1346 })
1347 .unwrap();
1348 let remove = encode_app_data_update_payload(&ComponentMutation::SuperAdminListRemove {
1349 inbox_id: &inbox,
1350 })
1351 .unwrap();
1352 let add_delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&add).unwrap();
1353 let remove_delta = TlsSetDelta::<InboxId>::tls_deserialize_exact(&remove).unwrap();
1354 assert!(matches!(&add_delta.mutations[0], TlsSetMutation::Insert(_)));
1355 assert!(matches!(
1356 &remove_delta.mutations[0],
1357 TlsSetMutation::Remove(_)
1358 ));
1359 }
1360
1361 #[xmtp_common::test]
1362 fn test_encode_admin_list_invalid_inbox_id() {
1363 let err = encode_app_data_update_payload(&ComponentMutation::AdminListAdd {
1366 inbox_id: "not-a-real-inbox-id",
1367 })
1368 .unwrap_err();
1369 assert!(
1370 matches!(
1371 err,
1372 ComponentSourceError::InvalidInboxId(InboxIdError::InvalidHex(_))
1373 ),
1374 "got {err:?}"
1375 );
1376 }
1377
1378 #[xmtp_common::test]
1381 fn test_apply_bytes_payload_returns_payload_verbatim() {
1382 let payload = b"new_name";
1383 let new_value = apply_app_data_update_payload(
1384 ComponentId::GROUP_NAME,
1385 payload,
1386 None,
1387 &empty_registry(),
1388 )
1389 .unwrap();
1390 assert_eq!(new_value, payload);
1391 }
1392
1393 #[xmtp_common::test]
1394 fn test_apply_bytes_payload_ignores_old_value() {
1395 let new_value = apply_app_data_update_payload(
1397 ComponentId::GROUP_DESCRIPTION,
1398 b"replacement",
1399 Some(b"old_description"),
1400 &empty_registry(),
1401 )
1402 .unwrap();
1403 assert_eq!(new_value, b"replacement");
1404 }
1405
1406 #[xmtp_common::test]
1407 fn test_apply_admin_list_insert_against_none() {
1408 let inbox = fake_inbox_id(0x44);
1411 let insert_payload =
1412 encode_app_data_update_payload(&ComponentMutation::AdminListAdd { inbox_id: &inbox })
1413 .unwrap();
1414
1415 let new_bytes = apply_app_data_update_payload(
1416 ComponentId::ADMIN_LIST,
1417 &insert_payload,
1418 None,
1419 &empty_registry(),
1420 )
1421 .unwrap();
1422
1423 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1424 assert_eq!(set.len(), 1);
1425 assert!(set.contains(&fake_inbox(0x44)));
1426 }
1427
1428 #[xmtp_common::test]
1429 fn test_apply_admin_list_insert_against_existing_set() {
1430 let alice = fake_inbox_id(0x01);
1433 let bob = fake_inbox_id(0x02);
1434
1435 let prior = encode_inbox_id_set(std::slice::from_ref(&alice)).unwrap();
1436 let insert_payload =
1437 encode_app_data_update_payload(&ComponentMutation::AdminListAdd { inbox_id: &bob })
1438 .unwrap();
1439
1440 let new_bytes = apply_app_data_update_payload(
1441 ComponentId::ADMIN_LIST,
1442 &insert_payload,
1443 Some(&prior),
1444 &empty_registry(),
1445 )
1446 .unwrap();
1447
1448 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1449 assert_eq!(set.len(), 2);
1450 assert!(set.contains(&fake_inbox(0x01)));
1451 assert!(set.contains(&fake_inbox(0x02)));
1452 }
1453
1454 #[xmtp_common::test]
1455 fn test_apply_admin_list_remove_against_existing_set() {
1456 let alice = fake_inbox_id(0x01);
1457 let bob = fake_inbox_id(0x02);
1458
1459 let prior = encode_inbox_id_set(&[alice.clone(), bob.clone()]).unwrap();
1460 let remove_payload = encode_app_data_update_payload(&ComponentMutation::AdminListRemove {
1461 inbox_id: &alice,
1462 })
1463 .unwrap();
1464
1465 let new_bytes = apply_app_data_update_payload(
1466 ComponentId::ADMIN_LIST,
1467 &remove_payload,
1468 Some(&prior),
1469 &empty_registry(),
1470 )
1471 .unwrap();
1472
1473 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1474 assert_eq!(set.len(), 1);
1475 assert!(set.contains(&fake_inbox(0x02)));
1476 assert!(!set.contains(&fake_inbox(0x01)));
1477 }
1478
1479 #[xmtp_common::test]
1480 fn test_apply_super_admin_list_delta() {
1481 let owner = fake_inbox_id(0xAA);
1482 let new_sa = fake_inbox_id(0xBB);
1483
1484 let prior = encode_inbox_id_set(std::slice::from_ref(&owner)).unwrap();
1485 let add_payload = encode_app_data_update_payload(&ComponentMutation::SuperAdminListAdd {
1486 inbox_id: &new_sa,
1487 })
1488 .unwrap();
1489
1490 let new_bytes = apply_app_data_update_payload(
1491 ComponentId::SUPER_ADMIN_LIST,
1492 &add_payload,
1493 Some(&prior),
1494 &empty_registry(),
1495 )
1496 .unwrap();
1497
1498 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1499 assert_eq!(set.len(), 2);
1500 assert!(set.contains(&fake_inbox(0xAA)));
1501 assert!(set.contains(&fake_inbox(0xBB)));
1502 }
1503
1504 #[xmtp_common::test]
1505 fn test_apply_admin_list_malformed_payload_returns_tls_codec_error() {
1506 let err = apply_app_data_update_payload(
1513 ComponentId::ADMIN_LIST,
1514 &[0xff, 0xff, 0xff, 0xff],
1515 None,
1516 &empty_registry(),
1517 )
1518 .unwrap_err();
1519 assert!(
1520 matches!(err, ComponentSourceError::TlsCodec(_)),
1521 "got {err:?}"
1522 );
1523 }
1524
1525 #[xmtp_common::test]
1526 fn test_apply_admin_list_malformed_old_value_returns_tls_codec_error() {
1527 let inbox = fake_inbox_id(0x55);
1530 let payload =
1531 encode_app_data_update_payload(&ComponentMutation::AdminListAdd { inbox_id: &inbox })
1532 .unwrap();
1533 let err = apply_app_data_update_payload(
1534 ComponentId::ADMIN_LIST,
1535 &payload,
1536 Some(&[0xde, 0xad, 0xbe, 0xef]),
1537 &empty_registry(),
1538 )
1539 .unwrap_err();
1540 assert!(
1541 matches!(err, ComponentSourceError::TlsCodec(_)),
1542 "got {err:?}"
1543 );
1544 }
1545
1546 #[xmtp_common::test]
1547 fn test_apply_immutable_first_insert_allowed() {
1548 let bytes = apply_app_data_update_payload(
1555 ComponentId::CONVERSATION_TYPE,
1556 b"seed",
1557 None,
1558 &empty_registry(),
1559 )
1560 .unwrap();
1561 assert_eq!(bytes, b"seed");
1562 }
1563
1564 #[xmtp_common::test]
1565 fn test_apply_immutable_overwrite_rejected() {
1566 let err = apply_app_data_update_payload(
1571 ComponentId::CONVERSATION_TYPE,
1572 b"junk",
1573 Some(b"prior"),
1574 &empty_registry(),
1575 )
1576 .unwrap_err();
1577 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1578 }
1579
1580 #[xmtp_common::test]
1581 fn test_apply_component_registry_delta_against_empty() {
1582 let id_a = ComponentId::GROUP_NAME;
1588 let id_b = ComponentId::GROUP_DESCRIPTION;
1589 let meta_a = registry_with(id_a, ComponentType::String)
1590 .get(&id_a)
1591 .unwrap()
1592 .unwrap()
1593 .encode_to_vec();
1594 let meta_b = registry_with(id_b, ComponentType::Bytes)
1595 .get(&id_b)
1596 .unwrap()
1597 .unwrap()
1598 .encode_to_vec();
1599 let delta = TlsMapDelta::<ComponentId, VLBytes>::new()
1600 .insert(id_a, VLBytes::new(meta_a.clone()))
1601 .insert(id_b, VLBytes::new(meta_b.clone()));
1602 let payload = delta.tls_serialize_detached().unwrap();
1603
1604 let new_bytes = apply_app_data_update_payload(
1605 ComponentId::COMPONENT_REGISTRY,
1606 &payload,
1607 None,
1608 &empty_registry(),
1609 )
1610 .unwrap();
1611 let map = TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(&new_bytes).unwrap();
1612 assert_eq!(map.len(), 2);
1613 assert_eq!(
1614 map.get(&id_a).map(|v| v.as_slice()),
1615 Some(meta_a.as_slice())
1616 );
1617 assert_eq!(
1618 map.get(&id_b).map(|v| v.as_slice()),
1619 Some(meta_b.as_slice())
1620 );
1621 }
1622
1623 #[xmtp_common::test]
1624 fn test_apply_group_membership_delta_against_existing_map() {
1625 let alice = fake_inbox(0xAA);
1629 let bob = fake_inbox(0xBB);
1630 let mut prior: TlsMap<InboxId, VLBytes> = TlsMap::new();
1631 prior.set(alice, VLBytes::new(vec![0x01]));
1632 prior.set(bob, VLBytes::new(vec![0x02]));
1633 let prior_bytes = prior.tls_serialize_detached().unwrap();
1634
1635 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
1636 .update(alice, VLBytes::new(vec![0x99]))
1637 .delete(bob);
1638 let payload = delta.tls_serialize_detached().unwrap();
1639
1640 let new_bytes = apply_app_data_update_payload(
1641 ComponentId::GROUP_MEMBERSHIP,
1642 &payload,
1643 Some(&prior_bytes),
1644 &empty_registry(),
1645 )
1646 .unwrap();
1647 let map = TlsMap::<InboxId, VLBytes>::tls_deserialize_exact(&new_bytes).unwrap();
1648 assert_eq!(map.len(), 1);
1649 assert_eq!(
1650 map.get(&alice).map(|v| v.as_slice()),
1651 Some([0x99].as_slice())
1652 );
1653 assert!(!map.contains_key(&bob));
1654 }
1655
1656 #[xmtp_common::test]
1657 fn test_apply_map_component_malformed_delta_returns_codec_error() {
1658 let err = apply_app_data_update_payload(
1661 ComponentId::COMPONENT_REGISTRY,
1662 &[0xff, 0xff, 0xff, 0xff],
1663 None,
1664 &empty_registry(),
1665 )
1666 .unwrap_err();
1667 assert!(
1668 matches!(err, ComponentSourceError::TlsCodec(_)),
1669 "got {err:?}"
1670 );
1671 }
1672
1673 #[xmtp_common::test]
1674 fn test_apply_map_component_apply_failure_surfaces_apply_error() {
1675 let alice = fake_inbox(0x01);
1678 let delta = TlsMapDelta::<InboxId, VLBytes>::new().update(alice, VLBytes::new(vec![0x42]));
1679 let payload = delta.tls_serialize_detached().unwrap();
1680 let err = apply_app_data_update_payload(
1681 ComponentId::GROUP_MEMBERSHIP,
1682 &payload,
1683 None,
1684 &empty_registry(),
1685 )
1686 .unwrap_err();
1687 assert!(
1688 matches!(
1689 err,
1690 ComponentSourceError::TlsMapApply(TlsMapError::KeyNotFound)
1691 ),
1692 "got {err:?}"
1693 );
1694 }
1695
1696 #[xmtp_common::test]
1709 fn test_apply_unknown_id_with_bytes_registry_entry_stores_payload() {
1710 let id = ComponentId::new(0xC123);
1714 let registry = registry_with(id, ComponentType::Bytes);
1715 let new_value = apply_app_data_update_payload(id, b"opaque", None, ®istry).unwrap();
1716 assert_eq!(new_value, b"opaque");
1717 }
1718
1719 #[xmtp_common::test]
1720 fn test_apply_unknown_id_with_string_registry_entry_validates_utf8() {
1721 let id = ComponentId::new(0xC222);
1725 let registry = registry_with(id, ComponentType::String);
1726 let ok = apply_app_data_update_payload(id, b"hello", None, ®istry).unwrap();
1727 assert_eq!(ok, b"hello");
1728 let err = apply_app_data_update_payload(id, &[0xC3, 0x28], None, ®istry).unwrap_err();
1729 assert!(matches!(
1730 err,
1731 ComponentSourceError::MalformedComponentValue { .. }
1732 ));
1733 }
1734
1735 #[xmtp_common::test]
1736 fn test_apply_unknown_id_with_tls_set_inbox_id_applies_delta() {
1737 let id = ComponentId::new(0xC333);
1742 let registry = registry_with(id, ComponentType::TlsSetInboxId);
1743 let bob = fake_inbox(0x02);
1744 let delta = TlsSetDelta::<InboxId>::new().insert(bob);
1745 let payload = delta.tls_serialize_detached().unwrap();
1746 let new_bytes = apply_app_data_update_payload(id, &payload, None, ®istry).unwrap();
1747 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1748 assert_eq!(set.len(), 1);
1749 assert!(set.contains(&bob));
1750 }
1751
1752 #[xmtp_common::test]
1753 fn test_apply_unknown_id_with_no_registry_entry_rejected() {
1754 let err =
1760 apply_app_data_update_payload(ComponentId::new(0xC456), b"x", None, &empty_registry())
1761 .unwrap_err();
1762 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1763 }
1764
1765 #[xmtp_common::test]
1774 fn test_apply_unknown_id_in_xmtp_immutable_range_first_write_allowed() {
1775 let id = ComponentId::new(0xBE05); assert!(id.is_immutable());
1777 let registry = registry_with(id, ComponentType::Bytes);
1778 let new_value = apply_app_data_update_payload(id, b"seed", None, ®istry).unwrap();
1779 assert_eq!(new_value, b"seed");
1780 }
1781
1782 #[xmtp_common::test]
1783 fn test_apply_unknown_id_in_xmtp_immutable_range_overwrite_rejected() {
1784 let id = ComponentId::new(0xBE05);
1785 assert!(id.is_immutable());
1786 let registry = registry_with(id, ComponentType::Bytes);
1787 let err = apply_app_data_update_payload(id, b"new", Some(b"old"), ®istry).unwrap_err();
1788 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1789 }
1790
1791 #[xmtp_common::test]
1796 fn test_apply_unknown_id_in_app_immutable_range_overwrite_rejected() {
1797 let id = ComponentId::new(0xFD42); assert!(id.is_immutable());
1799 let registry = registry_with(id, ComponentType::Bytes);
1800 let err = apply_app_data_update_payload(id, b"new", Some(b"old"), ®istry).unwrap_err();
1801 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1802 }
1803
1804 #[xmtp_common::test]
1805 fn test_apply_reserved_range_component_rejected_with_or_without_registry() {
1806 let err =
1811 apply_app_data_update_payload(ComponentId::new(0xFF01), b"x", None, &empty_registry())
1812 .unwrap_err();
1813 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1814 }
1815
1816 #[xmtp_common::test]
1817 fn test_apply_out_of_range_component_rejected() {
1818 let err =
1820 apply_app_data_update_payload(ComponentId::new(0x0042), b"x", None, &empty_registry())
1821 .unwrap_err();
1822 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1823 }
1824
1825 #[xmtp_common::test]
1826 fn test_apply_unknown_xmtp_range_id_with_registry_dispatches() {
1827 let id = ComponentId::new(0x8FFF);
1831 assert!(id.is_xmtp_range());
1832 let registry = registry_with(id, ComponentType::Bytes);
1833 let new_value = apply_app_data_update_payload(id, b"opaque", None, ®istry).unwrap();
1834 assert_eq!(new_value, b"opaque");
1835 }
1836
1837 fn update_op(delta: TlsSetDelta<InboxId>) -> AppDataUpdateOperation {
1842 AppDataUpdateOperation::Update(delta.tls_serialize_detached().unwrap().into())
1843 }
1844
1845 #[xmtp_common::test]
1846 fn test_expand_insert_surfaces_inbox_id_bytes() {
1847 let alice = fake_inbox(0x11);
1848 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1849 mutations: vec![TlsSetMutation::Insert(alice)],
1850 };
1851 let changes = expand_app_data_update_to_changes(
1852 ComponentId::ADMIN_LIST,
1853 &update_op(delta),
1854 None,
1855 &empty_registry(),
1856 )
1857 .unwrap();
1858 assert_eq!(changes.len(), 1);
1859 assert_eq!(changes[0].op, ComponentOp::Insert);
1860 assert_eq!(
1861 changes[0].value.as_deref(),
1862 Some(alice.as_bytes().as_slice())
1863 );
1864 }
1865
1866 #[xmtp_common::test]
1867 fn test_expand_remove_surfaces_inbox_id_bytes() {
1868 let alice = fake_inbox(0x22);
1869 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1870 mutations: vec![TlsSetMutation::Remove(alice)],
1871 };
1872 let changes = expand_app_data_update_to_changes(
1873 ComponentId::ADMIN_LIST,
1874 &update_op(delta),
1875 None,
1876 &empty_registry(),
1877 )
1878 .unwrap();
1879 assert_eq!(changes.len(), 1);
1880 assert_eq!(changes[0].op, ComponentOp::Delete);
1881 assert_eq!(
1882 changes[0].value.as_deref(),
1883 Some(alice.as_bytes().as_slice())
1884 );
1885 }
1886
1887 #[xmtp_common::test]
1888 fn test_expand_remove_by_hash_resolves_to_inbox_id_from_old_value() {
1889 let alice = fake_inbox(0xAA);
1893 let bob = fake_inbox(0xBB);
1894 let prior: TlsSet<InboxId> = [alice, bob].into_iter().collect();
1895 let old_bytes = prior.tls_serialize_detached().unwrap();
1896
1897 let hash = TlsKeyHash::of(&alice).unwrap();
1898 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1899 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1900 };
1901 let changes = expand_app_data_update_to_changes(
1902 ComponentId::ADMIN_LIST,
1903 &update_op(delta),
1904 Some(&old_bytes),
1905 &empty_registry(),
1906 )
1907 .unwrap();
1908 assert_eq!(changes.len(), 1);
1909 assert_eq!(changes[0].op, ComponentOp::Delete);
1910 assert_eq!(
1911 changes[0].value.as_deref(),
1912 Some(alice.as_bytes().as_slice())
1913 );
1914 }
1915
1916 #[xmtp_common::test]
1917 fn test_expand_remove_by_hash_miss_surfaces_none_value() {
1918 let alice = fake_inbox(0x01);
1922 let bob = fake_inbox(0x02);
1923 let prior: TlsSet<InboxId> = [alice].into_iter().collect();
1924 let old_bytes = prior.tls_serialize_detached().unwrap();
1925
1926 let hash = TlsKeyHash::of(&bob).unwrap();
1927 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1928 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1929 };
1930 let changes = expand_app_data_update_to_changes(
1931 ComponentId::ADMIN_LIST,
1932 &update_op(delta),
1933 Some(&old_bytes),
1934 &empty_registry(),
1935 )
1936 .unwrap();
1937 assert_eq!(changes.len(), 1);
1938 assert_eq!(changes[0].op, ComponentOp::Delete);
1939 assert!(changes[0].value.is_none());
1940 }
1941
1942 #[xmtp_common::test]
1943 fn test_expand_remove_by_hash_with_no_old_value_surfaces_none() {
1944 let hash = TlsKeyHash::of(&fake_inbox(0x33)).unwrap();
1946 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1947 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1948 };
1949 let changes = expand_app_data_update_to_changes(
1950 ComponentId::SUPER_ADMIN_LIST,
1951 &update_op(delta),
1952 None,
1953 &empty_registry(),
1954 )
1955 .unwrap();
1956 assert_eq!(changes.len(), 1);
1957 assert_eq!(changes[0].op, ComponentOp::Delete);
1958 assert!(changes[0].value.is_none());
1959 }
1960
1961 #[xmtp_common::test]
1962 fn test_expand_remove_by_hash_malformed_old_value_surfaces_codec_error() {
1963 let hash = TlsKeyHash::of(&fake_inbox(0x44)).unwrap();
1967 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1968 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1969 };
1970 let err = expand_app_data_update_to_changes(
1971 ComponentId::ADMIN_LIST,
1972 &update_op(delta),
1973 Some(&[0xde, 0xad, 0xbe, 0xef]),
1974 &empty_registry(),
1975 )
1976 .unwrap_err();
1977 assert!(
1978 matches!(err, ComponentSourceError::TlsCodec(_)),
1979 "got {err:?}"
1980 );
1981 }
1982
1983 #[xmtp_common::test]
1988 fn test_expand_unknown_component_bytes_typed_emits_single_update() {
1989 let id = ComponentId::new(0x80FF);
1990 let registry = registry_with(id, ComponentType::Bytes);
1991 let changes = expand_app_data_update_to_changes(
1992 id,
1993 &AppDataUpdateOperation::Update(b"opaque".to_vec().into()),
1994 None,
1995 ®istry,
1996 )
1997 .unwrap();
1998 assert_eq!(changes.len(), 1);
1999 assert_eq!(changes[0].op, ComponentOp::Update);
2000 assert_eq!(changes[0].value.as_deref(), Some(b"opaque".as_slice()));
2001 }
2002
2003 #[xmtp_common::test]
2006 fn test_expand_unknown_component_remove_emits_single_delete() {
2007 let id = ComponentId::new(0x80FF);
2008 let registry = registry_with(id, ComponentType::Bytes);
2009 let changes =
2010 expand_app_data_update_to_changes(id, &AppDataUpdateOperation::Remove, None, ®istry)
2011 .unwrap();
2012 assert_eq!(changes.len(), 1);
2013 assert_eq!(changes[0].op, ComponentOp::Delete);
2014 assert!(changes[0].value.is_none());
2015 }
2016
2017 #[xmtp_common::test]
2023 fn test_expand_unknown_component_tls_set_inbox_id_emits_per_element_changes() {
2024 let id = ComponentId::new(0x80FE);
2025 let registry = registry_with(id, ComponentType::TlsSetInboxId);
2026 let alice = fake_inbox(0x11);
2027 let bob = fake_inbox(0x22);
2028 let delta = TlsSetDelta::<InboxId>::new().insert(alice).insert(bob);
2029 let payload = delta.tls_serialize_detached().unwrap();
2030 let changes = expand_app_data_update_to_changes(
2031 id,
2032 &AppDataUpdateOperation::Update(payload.into()),
2033 None,
2034 ®istry,
2035 )
2036 .unwrap();
2037 assert_eq!(changes.len(), 2);
2038 assert!(changes.iter().all(|c| c.op == ComponentOp::Insert));
2039 }
2040
2041 #[xmtp_common::test]
2046 fn test_expand_reserved_range_id_still_rejected() {
2047 let err = expand_app_data_update_to_changes(
2048 ComponentId::new(0xFF02),
2049 &AppDataUpdateOperation::Update(b"x".to_vec().into()),
2050 None,
2051 &empty_registry(),
2052 )
2053 .unwrap_err();
2054 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
2055 }
2056
2057 #[xmtp_common::test]
2062 fn test_expand_unknown_component_no_registry_entry_rejected() {
2063 let err = expand_app_data_update_to_changes(
2064 ComponentId::new(0x80FF),
2065 &AppDataUpdateOperation::Update(b"opaque".to_vec().into()),
2066 None,
2067 &empty_registry(),
2068 )
2069 .unwrap_err();
2070 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
2071 }
2072
2073 #[xmtp_common::test]
2074 fn test_expand_skips_old_value_decode_when_no_remove_by_hash() {
2075 let alice = fake_inbox(0x55);
2078 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
2079 mutations: vec![TlsSetMutation::Insert(alice)],
2080 };
2081 let changes = expand_app_data_update_to_changes(
2082 ComponentId::ADMIN_LIST,
2083 &update_op(delta),
2084 Some(&[0xff, 0xff, 0xff]), &empty_registry(),
2086 )
2087 .unwrap();
2088 assert_eq!(changes.len(), 1);
2089 assert_eq!(changes[0].op, ComponentOp::Insert);
2090 }
2091
2092 use openmls::extensions::{
2101 AppDataDictionary, AppDataDictionaryExtension, Extension as OpenMlsExtension, Extensions,
2102 };
2103
2104 fn extensions_with_entries(
2110 migrated: bool,
2111 entries: &[(u16, Vec<u8>)],
2112 ) -> Extensions<openmls::group::GroupContext> {
2113 let mut dict = AppDataDictionary::new();
2114 if migrated {
2115 let _ = dict.insert(ComponentId::COMPONENT_REGISTRY.as_u16(), vec![0xCA; 4]);
2116 }
2117 for (id, bytes) in entries {
2118 let _ = dict.insert(*id, bytes.clone());
2119 }
2120 Extensions::from_vec(vec![OpenMlsExtension::AppDataDictionary(
2121 AppDataDictionaryExtension::new(dict),
2122 )])
2123 .expect("valid group-context extension set")
2124 }
2125
2126 #[xmtp_common::test]
2129 fn read_super_admin_list_unmigrated_returns_none() {
2130 let exts = extensions_with_entries(
2133 false,
2134 &[(
2135 ComponentId::SUPER_ADMIN_LIST.as_u16(),
2136 encode_inbox_id_set(&[fake_inbox_id(0x11)]).unwrap(),
2137 )],
2138 );
2139 assert!(
2140 read_super_admin_list_from_extensions(&exts)
2141 .unwrap()
2142 .is_none()
2143 );
2144 }
2145
2146 #[xmtp_common::test]
2147 fn read_super_admin_list_migrated_absent_returns_none() {
2148 let exts = extensions_with_entries(true, &[]);
2151 assert!(
2152 read_super_admin_list_from_extensions(&exts)
2153 .unwrap()
2154 .is_none()
2155 );
2156 }
2157
2158 #[xmtp_common::test]
2159 fn read_super_admin_list_migrated_happy_path() {
2160 let ids = vec![fake_inbox_id(0xAA), fake_inbox_id(0xBB)];
2161 let bytes = encode_inbox_id_set(&ids).unwrap();
2162 let exts =
2163 extensions_with_entries(true, &[(ComponentId::SUPER_ADMIN_LIST.as_u16(), bytes)]);
2164 let got = read_super_admin_list_from_extensions(&exts)
2165 .unwrap()
2166 .unwrap();
2167 assert_eq!(got.len(), 2);
2168 assert_eq!(got[0], fake_inbox_id(0xAA));
2170 assert_eq!(got[1], fake_inbox_id(0xBB));
2171 }
2172
2173 #[xmtp_common::test]
2174 fn read_super_admin_list_malformed_bytes_surface_error() {
2175 let exts = extensions_with_entries(
2176 true,
2177 &[(
2178 ComponentId::SUPER_ADMIN_LIST.as_u16(),
2179 vec![0x00, 0xDE, 0xAD],
2180 )],
2181 );
2182 let err = read_super_admin_list_from_extensions(&exts).unwrap_err();
2183 assert!(matches!(
2184 err,
2185 ComponentSourceError::MalformedComponentValue {
2186 component_id,
2187 ..
2188 } if component_id == ComponentId::SUPER_ADMIN_LIST
2189 ));
2190 }
2191
2192 fn encode_conv_type_bytes(value: i32) -> Vec<u8> {
2195 value.to_be_bytes().to_vec()
2196 }
2197
2198 fn encode_dm_pair(tag_a: u8, tag_b: u8) -> Vec<u8> {
2199 encode_inbox_id_set(&[fake_inbox_id(tag_a), fake_inbox_id(tag_b)]).unwrap()
2200 }
2201
2202 fn encode_creator_bytes(tag: u8) -> Vec<u8> {
2205 fake_inbox(tag).tls_serialize_detached().unwrap()
2206 }
2207
2208 #[xmtp_common::test]
2209 fn read_group_metadata_unmigrated_returns_none() {
2210 let exts = extensions_with_entries(
2211 false,
2212 &[
2213 (
2214 ComponentId::CONVERSATION_TYPE.as_u16(),
2215 encode_conv_type_bytes(1),
2216 ),
2217 (
2218 ComponentId::CREATOR_INBOX_ID.as_u16(),
2219 encode_creator_bytes(0x11),
2220 ),
2221 ],
2222 );
2223 assert!(
2224 read_group_metadata_from_extensions(&exts)
2225 .unwrap()
2226 .is_none()
2227 );
2228 }
2229
2230 #[xmtp_common::test]
2231 fn read_group_metadata_missing_required_seeds_returns_none() {
2232 let exts = extensions_with_entries(true, &[]);
2235 assert!(
2236 read_group_metadata_from_extensions(&exts)
2237 .unwrap()
2238 .is_none()
2239 );
2240 }
2241
2242 #[xmtp_common::test]
2243 fn read_group_metadata_happy_path_non_dm() {
2244 let exts = extensions_with_entries(
2245 true,
2246 &[
2247 (
2248 ComponentId::CONVERSATION_TYPE.as_u16(),
2249 encode_conv_type_bytes(1),
2250 ),
2251 (
2252 ComponentId::CREATOR_INBOX_ID.as_u16(),
2253 encode_creator_bytes(0x11),
2254 ),
2255 ],
2256 );
2257 let got = read_group_metadata_from_extensions(&exts).unwrap().unwrap();
2258 assert_eq!(got.conversation_type, 1);
2259 assert_eq!(got.creator_inbox_id, fake_inbox_id(0x11));
2260 assert!(got.dm_members.is_none());
2261 assert!(got.oneshot.is_none());
2262 }
2263
2264 #[xmtp_common::test]
2265 fn read_group_metadata_dm_happy_path() {
2266 let exts = extensions_with_entries(
2269 true,
2270 &[
2271 (
2272 ComponentId::CONVERSATION_TYPE.as_u16(),
2273 encode_conv_type_bytes(2),
2274 ),
2275 (
2276 ComponentId::CREATOR_INBOX_ID.as_u16(),
2277 encode_creator_bytes(0x22),
2278 ),
2279 (ComponentId::DM_MEMBERS.as_u16(), encode_dm_pair(0x22, 0x33)),
2280 ],
2281 );
2282 let got = read_group_metadata_from_extensions(&exts).unwrap().unwrap();
2283 let dm = got.dm_members.unwrap();
2284 assert_eq!(dm.dm_member_one.unwrap().inbox_id, fake_inbox_id(0x22));
2285 assert_eq!(dm.dm_member_two.unwrap().inbox_id, fake_inbox_id(0x33));
2286 }
2287
2288 #[xmtp_common::test]
2289 fn read_group_metadata_dm_wrong_cardinality_errors() {
2290 let one_element = encode_inbox_id_set(&[fake_inbox_id(0x44)]).unwrap();
2293 let exts = extensions_with_entries(
2294 true,
2295 &[
2296 (
2297 ComponentId::CONVERSATION_TYPE.as_u16(),
2298 encode_conv_type_bytes(2),
2299 ),
2300 (
2301 ComponentId::CREATOR_INBOX_ID.as_u16(),
2302 encode_creator_bytes(0x44),
2303 ),
2304 (ComponentId::DM_MEMBERS.as_u16(), one_element),
2305 ],
2306 );
2307 let err = read_group_metadata_from_extensions(&exts).unwrap_err();
2308 assert!(matches!(
2309 err,
2310 ComponentSourceError::MalformedComponentValue {
2311 component_id,
2312 ..
2313 } if component_id == ComponentId::DM_MEMBERS
2314 ));
2315 }
2316
2317 #[xmtp_common::test]
2318 fn read_group_metadata_malformed_creator_errors() {
2319 let exts = extensions_with_entries(
2325 true,
2326 &[
2327 (
2328 ComponentId::CONVERSATION_TYPE.as_u16(),
2329 encode_conv_type_bytes(1),
2330 ),
2331 (
2332 ComponentId::CREATOR_INBOX_ID.as_u16(),
2333 vec![0xFF, 0xFE, 0xFD],
2334 ),
2335 ],
2336 );
2337 let err = read_group_metadata_from_extensions(&exts).unwrap_err();
2338 assert!(matches!(
2339 err,
2340 ComponentSourceError::MalformedComponentValue {
2341 component_id,
2342 ..
2343 } if component_id == ComponentId::CREATOR_INBOX_ID
2344 ));
2345 }
2346
2347 #[xmtp_common::test]
2350 fn read_group_membership_unmigrated_returns_none() {
2351 use std::collections::BTreeMap;
2352 use xmtp_mls_common::app_data::migration::encode_group_membership_dict;
2353 use xmtp_proto::xmtp::mls::message_contents::{
2354 GroupMembershipEntry,
2355 group_membership_entry::{V1 as GroupMembershipEntryV1, Version},
2356 };
2357 let mut entries: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
2358 entries.insert(
2359 InboxId::from_bytes([0x11; INBOX_ID_BYTE_LEN]),
2360 GroupMembershipEntry {
2361 version: Some(Version::V1(GroupMembershipEntryV1 {
2362 sequence_id: 1,
2363 failed_installations: vec![],
2364 })),
2365 },
2366 );
2367 let bytes = encode_group_membership_dict(&entries).unwrap();
2368 let exts =
2369 extensions_with_entries(false, &[(ComponentId::GROUP_MEMBERSHIP.as_u16(), bytes)]);
2370 assert!(read_group_membership_from_dict(&exts).unwrap().is_none());
2371 }
2372
2373 #[xmtp_common::test]
2374 fn read_group_membership_happy_path_flattens_per_inbox() {
2375 use std::collections::BTreeMap;
2376 use xmtp_mls_common::app_data::migration::encode_group_membership_dict;
2377 use xmtp_proto::xmtp::mls::message_contents::{
2378 GroupMembershipEntry,
2379 group_membership_entry::{V1 as GroupMembershipEntryV1, Version},
2380 };
2381 let mut entries: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
2382 entries.insert(
2383 InboxId::from_bytes([0x11; INBOX_ID_BYTE_LEN]),
2384 GroupMembershipEntry {
2385 version: Some(Version::V1(GroupMembershipEntryV1 {
2386 sequence_id: 7,
2387 failed_installations: vec![vec![0xA1; 16]],
2388 })),
2389 },
2390 );
2391 entries.insert(
2392 InboxId::from_bytes([0x22; INBOX_ID_BYTE_LEN]),
2393 GroupMembershipEntry {
2394 version: Some(Version::V1(GroupMembershipEntryV1 {
2395 sequence_id: 42,
2396 failed_installations: vec![vec![0xB1; 16]],
2397 })),
2398 },
2399 );
2400 let bytes = encode_group_membership_dict(&entries).unwrap();
2401 let exts =
2402 extensions_with_entries(true, &[(ComponentId::GROUP_MEMBERSHIP.as_u16(), bytes)]);
2403
2404 let proto = read_group_membership_from_dict(&exts).unwrap().unwrap();
2405 assert_eq!(proto.members.len(), 2);
2407 assert_eq!(proto.members.get(&fake_inbox_id(0x11)), Some(&7));
2408 assert_eq!(proto.members.get(&fake_inbox_id(0x22)), Some(&42));
2409 assert_eq!(proto.failed_installations.len(), 2);
2411 }
2412
2413 #[xmtp_common::test]
2414 fn read_group_membership_malformed_bytes_surface_error() {
2415 let exts = extensions_with_entries(
2416 true,
2417 &[(
2418 ComponentId::GROUP_MEMBERSHIP.as_u16(),
2419 vec![0xDE, 0xAD, 0xBE, 0xEF],
2420 )],
2421 );
2422 let err = read_group_membership_from_dict(&exts).unwrap_err();
2423 assert!(matches!(
2424 err,
2425 ComponentSourceError::MalformedComponentValue {
2426 component_id,
2427 ..
2428 } if component_id == ComponentId::GROUP_MEMBERSHIP
2429 ));
2430 }
2431
2432 use xmtp_mls_common::group_mutable_metadata::{GroupMutableMetadata, MetadataField};
2455
2456 fn empty_base_gmm() -> GroupMutableMetadata {
2457 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new())
2458 }
2459
2460 #[xmtp_common::test]
2461 fn merge_with_malformed_registry_returns_valid_field() {
2462 let exts = extensions_with_entries(
2468 true, &[(ComponentId::GROUP_NAME.as_u16(), b"My Group".to_vec())],
2470 );
2471 let mut base = empty_base_gmm();
2472 merge_app_data_into_mutable_metadata_from_extensions(&mut base, &exts)
2473 .expect("merge ignores registry parseability and reads field directly");
2474 assert_eq!(
2475 base.attributes
2476 .get(MetadataField::GroupName.as_str())
2477 .map(String::as_str),
2478 Some("My Group"),
2479 );
2480 }
2481
2482 #[xmtp_common::test]
2483 fn merge_unmigrated_is_noop() {
2484 let exts = extensions_with_entries(
2488 false,
2489 &[(ComponentId::GROUP_NAME.as_u16(), b"Stray".to_vec())],
2490 );
2491 let mut base = empty_base_gmm();
2492 merge_app_data_into_mutable_metadata_from_extensions(&mut base, &exts).unwrap();
2493 assert!(
2494 !base
2495 .attributes
2496 .contains_key(MetadataField::GroupName.as_str()),
2497 "merge must be a no-op on unmigrated extensions"
2498 );
2499 }
2500
2501 #[xmtp_common::test]
2502 fn merge_with_malformed_field_surfaces_error_not_silent_loss() {
2503 let exts = extensions_with_entries(
2509 true,
2510 &[(ComponentId::ADMIN_LIST.as_u16(), vec![0xff, 0xff, 0xff])],
2511 );
2512 let mut base = empty_base_gmm();
2513 let err =
2514 merge_app_data_into_mutable_metadata_from_extensions(&mut base, &exts).unwrap_err();
2515 assert!(
2516 matches!(
2517 err,
2518 ComponentSourceError::MalformedComponentValue { component_id, .. }
2519 if component_id == ComponentId::ADMIN_LIST
2520 ),
2521 "expected MalformedComponentValue for ADMIN_LIST, got: {err:?}"
2522 );
2523 }
2524}