1use crate::{
32 app_data::{
33 component_id::ComponentId,
34 component_registry::ComponentRegistry,
35 components::type_dispatch::{apply_update_payload_for_type, expand_to_changes_for_type},
36 registry_table::lookup_component,
37 typed::ComponentTypedError,
38 },
39 group_mutable_metadata::{GroupMutableMetadata, GroupMutableMetadataError, MetadataField},
40 inbox_id::{InboxId, InboxIdError},
41 tls_map::{TlsMap, TlsMapDelta, TlsMapError, TlsMapMutation},
42 tls_set::{TlsSet, TlsSetDelta, TlsSetError, TlsSetMutation},
43};
44use openmls::{
45 extensions::Extensions,
46 group::{GroupContext, MlsGroup as OpenMlsGroup, StagedCommit},
47 messages::proposals::AppDataUpdateOperation,
48};
49use tls_codec::{Deserialize, Serialize, VLBytes};
50use xmtp_proto::xmtp::mls::message_contents::ComponentType;
51
52pub const MAX_FIELD_ELEMENT_BYTES: usize = 8192;
56
57pub const MAX_FIELD_SNAPSHOT_BYTES: usize = 65536;
60
61#[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}")]
84 MismatchedMutation(ComponentId),
85
86 #[error("invalid inbox id: {0}")]
91 InvalidInboxId(#[from] InboxIdError),
92
93 #[error("malformed value for component {component_id}: {reason}")]
98 MalformedComponentValue {
99 component_id: ComponentId,
101 reason: String,
103 },
104
105 #[error("unknown metadata field name: {0}")]
110 UnknownMetadataField(String),
111
112 #[error(transparent)]
115 GroupMutableMetadata(#[from] GroupMutableMetadataError),
116
117 #[error("tls codec error: {0}")]
119 TlsCodec(#[from] tls_codec::Error),
120
121 #[error("tls set apply error: {0}")]
124 TlsSetApply(#[from] TlsSetError),
125
126 #[error("tls map apply error: {0}")]
129 TlsMapApply(#[from] TlsMapError),
130
131 #[error("component {component_id} holds {len} bytes, over the {max}-byte bound")]
135 FieldBoundExceeded {
136 component_id: ComponentId,
138 len: usize,
140 max: usize,
142 },
143}
144
145impl ComponentSourceError {
146 pub fn component_id(&self) -> Option<ComponentId> {
152 match self {
153 Self::UnknownComponent(id)
154 | Self::NotImplemented(id)
155 | Self::ImmutableUpdate(id)
156 | Self::MismatchedMutation(id)
157 | Self::MalformedComponentValue {
158 component_id: id, ..
159 }
160 | Self::FieldBoundExceeded {
161 component_id: id, ..
162 } => Some(*id),
163 _ => None,
164 }
165 }
166}
167
168impl From<ComponentTypedError> for ComponentSourceError {
169 fn from(err: ComponentTypedError) -> Self {
175 match err {
176 ComponentTypedError::ImmutableUpdate(id) => Self::ImmutableUpdate(id),
177 ComponentTypedError::MismatchedMutation(id) => Self::MismatchedMutation(id),
178 ComponentTypedError::MalformedValue {
179 component_id,
180 reason,
181 } => Self::MalformedComponentValue {
182 component_id,
183 reason,
184 },
185 ComponentTypedError::InvalidInboxId(e) => Self::InvalidInboxId(e),
186 ComponentTypedError::TlsCodec(e) => Self::TlsCodec(e),
187 ComponentTypedError::TlsSetApply(e) => Self::TlsSetApply(e),
188 ComponentTypedError::TlsMapApply(e) => Self::TlsMapApply(e),
189 ComponentTypedError::UnspecifiedType(id) => Self::MalformedComponentValue {
190 component_id: id,
191 reason: "registered ComponentType is Unspecified".to_string(),
192 },
193 ComponentTypedError::RegistryMutation(e) => Self::MalformedComponentValue {
199 component_id: ComponentId::COMPONENT_REGISTRY,
200 reason: e.to_string(),
201 },
202 }
203 }
204}
205
206impl From<ComponentSourceError> for GroupMutableMetadataError {
207 fn from(err: ComponentSourceError) -> Self {
219 if let ComponentSourceError::GroupMutableMetadata(inner) = err {
220 return inner;
221 }
222 let component_id = err.component_id();
223 GroupMutableMetadataError::MalformedComponent {
224 component_id,
225 reason: err.to_string(),
226 }
227 }
228}
229
230pub fn component_type(id: ComponentId) -> Option<ComponentType> {
234 match id {
235 ComponentId::COMPONENT_REGISTRY => Some(ComponentType::TlsMapBytesBytes),
239 ComponentId::SUPER_ADMIN_LIST => Some(ComponentType::TlsSetInboxId),
240 ComponentId::ADMIN_LIST => Some(ComponentType::TlsSetInboxId),
241
242 ComponentId::GROUP_MEMBERSHIP => Some(ComponentType::TlsMapInboxIdBytes),
244 ComponentId::USER_DISPLAY_NAME => Some(ComponentType::TlsMapInboxIdString),
245
246 ComponentId::GROUP_NAME
248 | ComponentId::GROUP_DESCRIPTION
249 | ComponentId::GROUP_IMAGE_URL
250 | ComponentId::APP_DATA
251 | ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION => Some(ComponentType::String),
252
253 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
255 | ComponentId::MESSAGE_DISAPPEAR_IN_NS
256 | ComponentId::COMMIT_LOG_SIGNER
257 | ComponentId::GROUP_IMAGE => Some(ComponentType::Bytes),
258
259 ComponentId::CONVERSATION_TYPE
262 | ComponentId::CREATOR_INBOX_ID
263 | ComponentId::ONESHOT_MESSAGE => Some(ComponentType::Bytes),
264 ComponentId::DM_MEMBERS => Some(ComponentType::TlsSetInboxId),
265
266 _ => None,
267 }
268}
269
270use crate::group_mutable_metadata::METADATA_FIELD_COMPONENT_MAP;
271
272pub fn metadata_field_to_component_id(field_name: &str) -> Option<ComponentId> {
277 METADATA_FIELD_COMPONENT_MAP
278 .iter()
279 .find(|(field, _)| field.as_str() == field_name)
280 .map(|(_, id)| *id)
281}
282
283pub fn component_id_to_metadata_field(id: ComponentId) -> Option<MetadataField> {
290 METADATA_FIELD_COMPONENT_MAP
291 .iter()
292 .find(|(_, component_id)| *component_id == id)
293 .map(|(field, _)| *field)
294}
295
296pub fn read_component_bytes(
298 id: ComponentId,
299 extensions: &Extensions<GroupContext>,
300) -> Result<Option<Vec<u8>>, ComponentSourceError> {
301 Ok(read_from_app_data_dict_from_extensions(id, extensions))
302}
303
304pub fn read_post_commit_component_bytes(
342 id: ComponentId,
343 mls_group: &OpenMlsGroup,
344 staged_commit: &StagedCommit,
345 registry: &ComponentRegistry,
346) -> Result<Option<Vec<u8>>, ComponentSourceError> {
347 let openmls_id: openmls::component::ComponentId = id.as_u16();
348
349 enum Op {
357 Update(Vec<u8>),
358 Remove,
359 }
360 let ops: Vec<Op> = staged_commit
361 .app_data_update_proposals()
362 .filter_map(|queued| {
363 let proposal = queued.app_data_update_proposal();
364 if proposal.component_id() != openmls_id {
365 return None;
366 }
367 Some(match proposal.operation() {
368 AppDataUpdateOperation::Update(payload) => Op::Update(payload.as_slice().to_vec()),
369 AppDataUpdateOperation::Remove => Op::Remove,
370 })
371 })
372 .collect();
373 if ops.is_empty() {
374 return Ok(read_from_app_data_dict(id, mls_group));
375 }
376
377 let mut current = read_from_app_data_dict(id, mls_group);
378 for op in &ops {
379 match op {
380 Op::Update(payload) => {
381 current = Some(apply_app_data_update_payload(
382 id,
383 payload,
384 current.as_deref(),
385 registry,
386 )?);
387 }
388 Op::Remove => current = None,
389 }
390 }
391 Ok(current)
392}
393
394pub fn read_from_app_data_dict(id: ComponentId, mls_group: &OpenMlsGroup) -> Option<Vec<u8>> {
404 read_from_app_data_dict_from_extensions(id, mls_group.extensions())
405}
406
407pub fn read_from_app_data_dict_from_extensions(
412 id: ComponentId,
413 extensions: &Extensions<GroupContext>,
414) -> Option<Vec<u8>> {
415 extensions
416 .app_data_dictionary()
417 .and_then(|ext| ext.dictionary().get(&id.as_u16()))
418 .map(|bytes| bytes.to_vec())
419}
420
421use crate::app_data::typed::ExpandedComponentChange;
422
423pub fn expand_app_data_update_to_changes(
452 component_id: ComponentId,
453 operation: &AppDataUpdateOperation,
454 old_value: Option<&[u8]>,
455 registry: &ComponentRegistry,
456) -> Result<Vec<ExpandedComponentChange>, ComponentSourceError> {
457 if let Some(component) = lookup_component(component_id) {
458 return component
459 .expand_to_changes(operation, old_value)
460 .map_err(Into::into);
461 }
462
463 let ty = resolved_type(component_id, registry)?;
469 expand_to_changes_for_type(component_id, ty, operation, old_value).map_err(Into::into)
470}
471
472pub fn apply_app_data_update_payload(
488 id: ComponentId,
489 payload: &[u8],
490 old_value: Option<&[u8]>,
491 registry: &ComponentRegistry,
492) -> Result<Vec<u8>, ComponentSourceError> {
493 check_update_payload_bounds(id, payload, registry)?;
494 if id.is_immutable() && old_value.is_some() {
501 return Err(ComponentSourceError::ImmutableUpdate(id));
502 }
503
504 let new_value = match lookup_component(id) {
507 Some(component) => component.apply_update_payload(payload, old_value)?,
508 None => {
509 apply_update_payload_for_type(id, resolved_type(id, registry)?, payload, old_value)?
510 }
511 };
512 if is_field_bounded(id) {
513 check_field_bounds(id, resolved_type(id, registry)?, &new_value)?;
514 }
515 Ok(new_value)
516}
517
518fn resolved_type(
527 id: ComponentId,
528 registry: &ComponentRegistry,
529) -> Result<ComponentType, ComponentSourceError> {
530 component_type(id).map_or_else(|| registered_component_type(id, registry), Ok)
531}
532
533fn is_field_bounded(id: ComponentId) -> bool {
537 id.is_app_range()
538 || [
539 ComponentId::USER_DISPLAY_NAME,
540 ComponentId::GROUP_IMAGE,
541 ComponentId::COMPONENT_REGISTRY,
542 ]
543 .contains(&id)
544}
545
546fn check_update_payload_bounds(
553 id: ComponentId,
554 payload: &[u8],
555 registry: &ComponentRegistry,
556) -> Result<(), ComponentSourceError> {
557 if !is_field_bounded(id) {
558 return Ok(());
559 }
560 let longest = match resolved_type(id, registry)? {
562 ComponentType::Bytes | ComponentType::String => Some(payload.len()),
563 ComponentType::TlsSetBytes => TlsSetDelta::<VLBytes>::tls_deserialize_exact(payload)?
564 .mutations
565 .iter()
566 .map(|m| match m {
567 TlsSetMutation::Insert(key) | TlsSetMutation::Remove(key) => key.as_slice().len(),
568 TlsSetMutation::RemoveByHash(_) => 0,
569 })
570 .max(),
571 ComponentType::TlsMapInboxIdBytes | ComponentType::TlsMapInboxIdString => {
572 longest_delta_value::<InboxId>(payload)?
573 }
574 ComponentType::TlsMapBytesBytes if id == ComponentId::COMPONENT_REGISTRY => {
575 longest_delta_value::<ComponentId>(payload)?
576 }
577 ComponentType::TlsMapBytesBytes => {
578 TlsMapDelta::<VLBytes, VLBytes>::tls_deserialize_exact(payload)?
579 .mutations
580 .iter()
581 .map(|m| match m {
582 TlsMapMutation::Insert { key, value }
583 | TlsMapMutation::Update { key, value } => {
584 key.as_slice().len().max(value.as_slice().len())
585 }
586 TlsMapMutation::Delete { key } => key.as_slice().len(),
587 })
588 .max()
589 }
590 ComponentType::TlsSetInboxId | ComponentType::Unspecified => None,
591 };
592 check_element_bound(id, longest.unwrap_or(0))
593}
594
595fn longest_delta_value<K>(payload: &[u8]) -> Result<Option<usize>, tls_codec::Error>
598where
599 TlsMapDelta<K, VLBytes>: Deserialize,
600{
601 Ok(TlsMapDelta::<K, VLBytes>::tls_deserialize_exact(payload)?
602 .mutations
603 .iter()
604 .map(|m| match m {
605 TlsMapMutation::Insert { value, .. } | TlsMapMutation::Update { value, .. } => {
606 value.as_slice().len()
607 }
608 TlsMapMutation::Delete { .. } => 0,
609 })
610 .max())
611}
612
613fn check_field_bounds(
620 id: ComponentId,
621 ty: ComponentType,
622 value: &[u8],
623) -> Result<(), ComponentSourceError> {
624 let longest_element = match ty {
625 ComponentType::Bytes | ComponentType::String => Some(value.len()),
626 _ if value.len() > MAX_FIELD_SNAPSHOT_BYTES => {
627 return Err(ComponentSourceError::FieldBoundExceeded {
628 component_id: id,
629 len: value.len(),
630 max: MAX_FIELD_SNAPSHOT_BYTES,
631 });
632 }
633 ComponentType::TlsSetBytes => TlsSet::<VLBytes>::tls_deserialize_exact(value)?
634 .iter()
635 .map(|v| v.as_slice().len())
636 .max(),
637 ComponentType::TlsMapInboxIdBytes | ComponentType::TlsMapInboxIdString => {
638 longest_map_value::<InboxId>(value)?
639 }
640 ComponentType::TlsMapBytesBytes if id == ComponentId::COMPONENT_REGISTRY => {
641 longest_map_value::<ComponentId>(value)?
642 }
643 ComponentType::TlsMapBytesBytes => {
644 TlsMap::<VLBytes, VLBytes>::tls_deserialize_exact(value)?
645 .iter()
646 .flat_map(|(k, v)| [k.as_slice().len(), v.as_slice().len()])
647 .max()
648 }
649 ComponentType::TlsSetInboxId | ComponentType::Unspecified => None,
650 }
651 .unwrap_or(0);
652 check_element_bound(id, longest_element)
653}
654
655fn longest_map_value<K: Ord>(value: &[u8]) -> Result<Option<usize>, tls_codec::Error>
657where
658 TlsMap<K, VLBytes>: Deserialize,
659{
660 Ok(TlsMap::<K, VLBytes>::tls_deserialize_exact(value)?
661 .values()
662 .map(|v| v.as_slice().len())
663 .max())
664}
665
666fn check_element_bound(id: ComponentId, len: usize) -> Result<(), ComponentSourceError> {
669 if len > MAX_FIELD_ELEMENT_BYTES {
670 return Err(ComponentSourceError::FieldBoundExceeded {
671 component_id: id,
672 len,
673 max: MAX_FIELD_ELEMENT_BYTES,
674 });
675 }
676 Ok(())
677}
678
679fn registered_component_type(
685 id: ComponentId,
686 registry: &ComponentRegistry,
687) -> Result<ComponentType, ComponentSourceError> {
688 let meta = registry
689 .get(&id)
690 .map_err(|e| ComponentSourceError::MalformedComponentValue {
691 component_id: id,
692 reason: format!("registry lookup: {e}"),
693 })?
694 .ok_or(ComponentSourceError::UnknownComponent(id))?;
695 ComponentType::try_from(meta.component_type).map_err(|_| {
696 ComponentSourceError::MalformedComponentValue {
697 component_id: id,
698 reason: format!(
699 "registry entry has unknown component_type tag {}",
700 meta.component_type
701 ),
702 }
703 })
704}
705
706pub fn merge_app_data_into_mutable_metadata(
731 base: &mut GroupMutableMetadata,
732 mls_group: &OpenMlsGroup,
733) -> Result<(), ComponentSourceError> {
734 merge_app_data_into_mutable_metadata_from_extensions(base, mls_group.extensions())
735}
736
737pub fn extract_group_mutable_metadata_capability_aware(
739 mls_group: &OpenMlsGroup,
740) -> Result<GroupMutableMetadata, ComponentSourceError> {
741 let mut base =
742 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new());
743 merge_app_data_into_mutable_metadata(&mut base, mls_group)?;
744 Ok(base)
745}
746
747pub fn extract_group_mutable_metadata_capability_aware_from_extensions(
753 extensions: &Extensions<GroupContext>,
754) -> Result<GroupMutableMetadata, ComponentSourceError> {
755 let mut base =
756 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new());
757 merge_app_data_into_mutable_metadata_from_extensions(&mut base, extensions)?;
758 Ok(base)
759}
760
761pub fn merge_app_data_into_mutable_metadata_from_extensions(
763 base: &mut GroupMutableMetadata,
764 extensions: &openmls::extensions::Extensions<openmls::group::GroupContext>,
765) -> Result<(), ComponentSourceError> {
766 crate::group_mutable_metadata::merge_dict_into_mutable_metadata(base, extensions).map_err(|e| {
773 match e {
774 GroupMutableMetadataError::MalformedComponent {
775 component_id: Some(component_id),
776 reason,
777 } => ComponentSourceError::MalformedComponentValue {
778 component_id,
779 reason,
780 },
781 other => ComponentSourceError::GroupMutableMetadata(other),
782 }
783 })
784}
785
786pub fn inbox_id_str_to_bytes(inbox_id: &str) -> Result<InboxId, ComponentSourceError> {
804 InboxId::from_hex(inbox_id).map_err(Into::into)
805}
806
807pub fn read_super_admin_list_from_dict(
809 mls_group: &OpenMlsGroup,
810) -> Result<Option<Vec<String>>, ComponentSourceError> {
811 read_super_admin_list_from_extensions(mls_group.extensions())
812}
813
814pub fn read_super_admin_list_from_extensions(
819 extensions: &Extensions<GroupContext>,
820) -> Result<Option<Vec<String>>, ComponentSourceError> {
821 let Some(ext) = extensions.app_data_dictionary() else {
822 return Ok(None);
823 };
824 let Some(bytes) = ext
825 .dictionary()
826 .get(&ComponentId::SUPER_ADMIN_LIST.as_u16())
827 else {
828 return Ok(None);
829 };
830 let set = TlsSet::<InboxId>::tls_deserialize_exact(bytes).map_err(|e| {
831 ComponentSourceError::MalformedComponentValue {
832 component_id: ComponentId::SUPER_ADMIN_LIST,
833 reason: format!("invalid TlsSet<InboxId>: {e}"),
834 }
835 })?;
836 Ok(Some(set.iter().map(|id| id.to_hex()).collect()))
837}
838
839pub fn read_group_metadata_from_dict(
858 mls_group: &OpenMlsGroup,
859) -> Result<Option<GroupMetadataReturn>, ComponentSourceError> {
860 read_group_metadata_from_extensions(mls_group.extensions())
861}
862
863pub fn read_group_metadata_from_extensions(
866 extensions: &Extensions<GroupContext>,
867) -> Result<Option<GroupMetadataReturn>, ComponentSourceError> {
868 use prost::Message;
869 use xmtp_proto::xmtp::mls::message_contents::{
870 ConversationType, DmMembers as DmMembersProto, Inbox as InboxProto, OneshotMessage,
871 };
872
873 let Some(ext) = extensions.app_data_dictionary() else {
874 return Ok(None);
875 };
876 let dict = ext.dictionary();
877
878 let Some(ct_bytes) = dict.get(&ComponentId::CONVERSATION_TYPE.as_u16()) else {
879 return Ok(None);
880 };
881 let Some(creator_bytes) = dict.get(&ComponentId::CREATOR_INBOX_ID.as_u16()) else {
882 return Ok(None);
883 };
884
885 let ct_arr: [u8; 4] =
886 ct_bytes
887 .try_into()
888 .map_err(|_| ComponentSourceError::MalformedComponentValue {
889 component_id: ComponentId::CONVERSATION_TYPE,
890 reason: format!("expected 4 bytes, got {}", ct_bytes.len()),
891 })?;
892 let conversation_type = i32::from_be_bytes(ct_arr);
893
894 let creator_inbox_id = InboxId::tls_deserialize_exact(creator_bytes)
895 .map_err(|e| ComponentSourceError::MalformedComponentValue {
896 component_id: ComponentId::CREATOR_INBOX_ID,
897 reason: format!("invalid InboxId TLS encoding: {e}"),
898 })?
899 .to_hex();
900
901 let dm_members = match dict.get(&ComponentId::DM_MEMBERS.as_u16()) {
906 Some(_) if conversation_type != ConversationType::Dm as i32 => {
907 return Err(ComponentSourceError::MalformedComponentValue {
908 component_id: ComponentId::DM_MEMBERS,
909 reason: format!("present in conversation type {conversation_type}"),
910 });
911 }
912 Some(b) => {
913 let set = TlsSet::<InboxId>::tls_deserialize_exact(b).map_err(|e| {
914 ComponentSourceError::MalformedComponentValue {
915 component_id: ComponentId::DM_MEMBERS,
916 reason: format!("invalid TlsSet<InboxId>: {e}"),
917 }
918 })?;
919 let ids: Vec<InboxId> = set.iter().copied().collect();
920 if ids.len() != 2 {
921 return Err(ComponentSourceError::MalformedComponentValue {
922 component_id: ComponentId::DM_MEMBERS,
923 reason: format!("expected 2 inbox ids, got {}", ids.len()),
924 });
925 }
926 Some(DmMembersProto {
927 dm_member_one: Some(InboxProto {
928 inbox_id: ids[0].to_hex(),
929 }),
930 dm_member_two: Some(InboxProto {
931 inbox_id: ids[1].to_hex(),
932 }),
933 })
934 }
935 None => None,
936 };
937
938 let oneshot = match dict.get(&ComponentId::ONESHOT_MESSAGE.as_u16()) {
939 Some(b) => Some(OneshotMessage::decode(b).map_err(|e| {
940 ComponentSourceError::MalformedComponentValue {
941 component_id: ComponentId::ONESHOT_MESSAGE,
942 reason: format!("OneshotMessage prost decode: {e}"),
943 }
944 })?),
945 None => None,
946 };
947
948 Ok(Some(GroupMetadataReturn {
949 conversation_type,
950 creator_inbox_id,
951 dm_members,
952 oneshot,
953 }))
954}
955
956#[derive(Debug)]
959pub struct GroupMetadataReturn {
960 pub conversation_type: i32,
961 pub creator_inbox_id: String,
962 pub dm_members: Option<xmtp_proto::xmtp::mls::message_contents::DmMembers>,
963 pub oneshot: Option<xmtp_proto::xmtp::mls::message_contents::OneshotMessage>,
964}
965
966pub fn read_group_membership_from_dict(
972 extensions: &Extensions<GroupContext>,
973) -> Result<Option<xmtp_proto::xmtp::mls::message_contents::GroupMembership>, ComponentSourceError>
974{
975 use crate::app_data::migration::decode_group_membership_dict;
976 use xmtp_proto::xmtp::mls::message_contents::GroupMembership as GroupMembershipProto;
977
978 let Some(ext) = extensions.app_data_dictionary() else {
979 return Ok(None);
980 };
981 let Some(bytes) = ext
982 .dictionary()
983 .get(&ComponentId::GROUP_MEMBERSHIP.as_u16())
984 else {
985 return Ok(None);
986 };
987
988 let entries = decode_group_membership_dict(bytes).map_err(|e| {
989 ComponentSourceError::MalformedComponentValue {
990 component_id: ComponentId::GROUP_MEMBERSHIP,
991 reason: format!("TlsMap decode: {e}"),
992 }
993 })?;
994
995 use xmtp_proto::xmtp::mls::message_contents::group_membership_entry::Version as GroupMembershipEntryVersion;
1007 let mut members: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
1008 let mut failed: Vec<Vec<u8>> = Vec::new();
1009 for (inbox_id, entry) in entries {
1010 let v1 = match entry.version {
1011 Some(GroupMembershipEntryVersion::V1(v1)) => v1,
1012 None => {
1013 return Err(ComponentSourceError::MalformedComponentValue {
1014 component_id: ComponentId::GROUP_MEMBERSHIP,
1015 reason: format!(
1016 "GroupMembershipEntry for {} has no version",
1017 inbox_id.to_hex()
1018 ),
1019 });
1020 }
1021 };
1022 members.insert(inbox_id.to_hex(), v1.sequence_id);
1023 failed.extend(v1.failed_installations);
1024 }
1025
1026 Ok(Some(GroupMembershipProto {
1027 members,
1028 failed_installations: failed,
1029 }))
1030}
1031
1032#[cfg_attr(not(test), expect(dead_code))]
1036fn encode_inbox_id_set(inbox_ids: &[String]) -> Result<Vec<u8>, ComponentSourceError> {
1037 let ids: Vec<InboxId> = inbox_ids
1038 .iter()
1039 .map(|s| inbox_id_str_to_bytes(s))
1040 .collect::<Result<Vec<_>, _>>()?;
1041 let set: TlsSet<InboxId> = ids.into_iter().collect();
1042 Ok(set.tls_serialize_detached()?)
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048 use crate::{
1049 app_data::{
1050 component_permissions::component_permissions,
1051 component_registry::{ComponentOp, new_component_metadata},
1052 },
1053 inbox_id::INBOX_ID_BYTE_LEN,
1054 tls_set::TlsKeyHash,
1055 };
1056 use prost::Message;
1057 use xmtp_proto::xmtp::mls::message_contents::{
1058 MetadataPolicy as MetadataPolicyProto,
1059 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
1060 };
1061
1062 fn fake_inbox_id(tag: u8) -> String {
1066 hex::encode([tag; INBOX_ID_BYTE_LEN])
1067 }
1068
1069 fn fake_inbox(tag: u8) -> InboxId {
1071 InboxId::from_bytes([tag; INBOX_ID_BYTE_LEN])
1072 }
1073
1074 fn empty_registry() -> ComponentRegistry {
1078 ComponentRegistry::new()
1079 }
1080
1081 fn inbox_set_delta(mutation: TlsSetMutation<InboxId>) -> Vec<u8> {
1083 TlsSetDelta {
1084 mutations: vec![mutation],
1085 }
1086 .tls_serialize_detached()
1087 .unwrap()
1088 }
1089
1090 fn registry_with(id: ComponentId, ty: ComponentType) -> ComponentRegistry {
1095 fn allow() -> MetadataPolicyProto {
1096 MetadataPolicyProto {
1097 kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Allow as i32)),
1098 }
1099 }
1100 let perms = component_permissions()
1101 .insert(allow())
1102 .update(allow())
1103 .delete(allow())
1104 .call();
1105 let meta = new_component_metadata(perms, ty);
1106 let mut reg = ComponentRegistry::new();
1107 reg.set(id, meta).unwrap();
1108 reg
1109 }
1110
1111 #[xmtp_common::test]
1114 fn test_inbox_id_round_trip() {
1115 let original = fake_inbox_id(0xAB);
1116 let id = inbox_id_str_to_bytes(&original).unwrap();
1117 assert_eq!(id.as_bytes(), &[0xAB; 32]);
1118 assert_eq!(id.to_hex(), original);
1119 }
1120
1121 #[xmtp_common::test]
1122 fn test_inbox_id_invalid_hex() {
1123 let err = inbox_id_str_to_bytes("not_hex").unwrap_err();
1124 assert!(
1125 matches!(
1126 err,
1127 ComponentSourceError::InvalidInboxId(InboxIdError::InvalidHex(_))
1128 ),
1129 "got {err:?}"
1130 );
1131 }
1132
1133 #[xmtp_common::test]
1134 fn test_inbox_id_wrong_length() {
1135 let err = inbox_id_str_to_bytes(&"ab".repeat(16)).unwrap_err();
1137 assert!(
1138 matches!(
1139 err,
1140 ComponentSourceError::InvalidInboxId(InboxIdError::InvalidLength {
1141 expected: INBOX_ID_BYTE_LEN,
1142 actual: 16,
1143 })
1144 ),
1145 "got {err:?}"
1146 );
1147 }
1148
1149 #[xmtp_common::test]
1152 fn test_component_type_string_family() {
1153 for id in [
1156 ComponentId::GROUP_NAME,
1157 ComponentId::GROUP_DESCRIPTION,
1158 ComponentId::GROUP_IMAGE_URL,
1159 ComponentId::APP_DATA,
1160 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
1161 ] {
1162 assert_eq!(component_type(id), Some(ComponentType::String));
1163 }
1164 }
1165
1166 #[xmtp_common::test]
1167 fn test_component_type_bytes_family() {
1168 for id in [
1171 ComponentId::MESSAGE_DISAPPEAR_FROM_NS,
1172 ComponentId::MESSAGE_DISAPPEAR_IN_NS,
1173 ComponentId::COMMIT_LOG_SIGNER,
1174 ] {
1175 assert_eq!(component_type(id), Some(ComponentType::Bytes));
1176 }
1177 }
1178
1179 #[xmtp_common::test]
1180 fn test_component_type_set_inbox_id_family() {
1181 for id in [
1182 ComponentId::ADMIN_LIST,
1183 ComponentId::SUPER_ADMIN_LIST,
1184 ComponentId::DM_MEMBERS,
1185 ] {
1186 assert_eq!(component_type(id), Some(ComponentType::TlsSetInboxId));
1187 }
1188 }
1189
1190 #[xmtp_common::test]
1191 fn test_component_type_group_membership() {
1192 assert_eq!(
1193 component_type(ComponentId::GROUP_MEMBERSHIP),
1194 Some(ComponentType::TlsMapInboxIdBytes)
1195 );
1196 }
1197
1198 #[xmtp_common::test]
1203 fn test_component_type_profile_and_image() {
1204 assert_eq!(
1205 component_type(ComponentId::USER_DISPLAY_NAME),
1206 Some(ComponentType::TlsMapInboxIdString)
1207 );
1208 assert_eq!(
1209 component_type(ComponentId::GROUP_IMAGE),
1210 Some(ComponentType::Bytes)
1211 );
1212 }
1213
1214 #[xmtp_common::test]
1215 fn test_component_type_app_range_is_none() {
1216 assert_eq!(component_type(ComponentId::new(0xC000)), None);
1217 assert_eq!(component_type(ComponentId::new(0xFDAB)), None);
1218 }
1219
1220 #[xmtp_common::test]
1223 fn test_metadata_field_round_trip() {
1224 for field in [
1225 MetadataField::GroupName,
1226 MetadataField::Description,
1227 MetadataField::GroupImageUrlSquare,
1228 MetadataField::MessageDisappearFromNS,
1229 MetadataField::MessageDisappearInNS,
1230 MetadataField::MinimumSupportedProtocolVersion,
1231 MetadataField::CommitLogSigner,
1232 MetadataField::AppData,
1233 ] {
1234 let id = metadata_field_to_component_id(field.as_str())
1235 .expect("every MetadataField has a ComponentId");
1236 let back = component_id_to_metadata_field(id)
1237 .expect("every mapped ComponentId has a MetadataField");
1238 assert_eq!(back, field, "round-trip mismatch for {field:?}");
1239 }
1240 }
1241
1242 #[xmtp_common::test]
1243 fn test_metadata_field_unknown_name_returns_none() {
1244 assert!(metadata_field_to_component_id("nonexistent_field").is_none());
1245 }
1246
1247 #[xmtp_common::test]
1248 fn test_component_id_to_metadata_field_non_gmm_returns_none() {
1249 assert!(component_id_to_metadata_field(ComponentId::ADMIN_LIST).is_none());
1250 assert!(component_id_to_metadata_field(ComponentId::GROUP_MEMBERSHIP).is_none());
1251 assert!(component_id_to_metadata_field(ComponentId::new(0xC000)).is_none());
1252 }
1253
1254 #[xmtp_common::test]
1257 fn test_apply_bytes_payload_returns_payload_verbatim() {
1258 let payload = b"new_name";
1259 let new_value = apply_app_data_update_payload(
1260 ComponentId::GROUP_NAME,
1261 payload,
1262 None,
1263 &empty_registry(),
1264 )
1265 .unwrap();
1266 assert_eq!(new_value, payload);
1267 }
1268
1269 #[xmtp_common::test]
1270 fn test_apply_bytes_payload_ignores_old_value() {
1271 let new_value = apply_app_data_update_payload(
1273 ComponentId::GROUP_DESCRIPTION,
1274 b"replacement",
1275 Some(b"old_description"),
1276 &empty_registry(),
1277 )
1278 .unwrap();
1279 assert_eq!(new_value, b"replacement");
1280 }
1281
1282 #[xmtp_common::test]
1283 fn test_apply_admin_list_insert_against_none() {
1284 let inbox = fake_inbox_id(0x44);
1287 let insert_payload =
1288 inbox_set_delta(TlsSetMutation::Insert(InboxId::from_hex(&inbox).unwrap()));
1289
1290 let new_bytes = apply_app_data_update_payload(
1291 ComponentId::ADMIN_LIST,
1292 &insert_payload,
1293 None,
1294 &empty_registry(),
1295 )
1296 .unwrap();
1297
1298 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1299 assert_eq!(set.len(), 1);
1300 assert!(set.contains(&fake_inbox(0x44)));
1301 }
1302
1303 #[xmtp_common::test]
1304 fn test_apply_admin_list_insert_against_existing_set() {
1305 let alice = fake_inbox_id(0x01);
1308 let bob = fake_inbox_id(0x02);
1309
1310 let prior = encode_inbox_id_set(std::slice::from_ref(&alice)).unwrap();
1311 let insert_payload =
1312 inbox_set_delta(TlsSetMutation::Insert(InboxId::from_hex(&bob).unwrap()));
1313
1314 let new_bytes = apply_app_data_update_payload(
1315 ComponentId::ADMIN_LIST,
1316 &insert_payload,
1317 Some(&prior),
1318 &empty_registry(),
1319 )
1320 .unwrap();
1321
1322 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1323 assert_eq!(set.len(), 2);
1324 assert!(set.contains(&fake_inbox(0x01)));
1325 assert!(set.contains(&fake_inbox(0x02)));
1326 }
1327
1328 #[xmtp_common::test]
1329 fn test_apply_admin_list_remove_against_existing_set() {
1330 let alice = fake_inbox_id(0x01);
1331 let bob = fake_inbox_id(0x02);
1332
1333 let prior = encode_inbox_id_set(&[alice.clone(), bob.clone()]).unwrap();
1334 let remove_payload =
1335 inbox_set_delta(TlsSetMutation::Remove(InboxId::from_hex(&alice).unwrap()));
1336
1337 let new_bytes = apply_app_data_update_payload(
1338 ComponentId::ADMIN_LIST,
1339 &remove_payload,
1340 Some(&prior),
1341 &empty_registry(),
1342 )
1343 .unwrap();
1344
1345 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1346 assert_eq!(set.len(), 1);
1347 assert!(set.contains(&fake_inbox(0x02)));
1348 assert!(!set.contains(&fake_inbox(0x01)));
1349 }
1350
1351 #[xmtp_common::test]
1352 fn test_apply_super_admin_list_delta() {
1353 let owner = fake_inbox_id(0xAA);
1354 let new_sa = fake_inbox_id(0xBB);
1355
1356 let prior = encode_inbox_id_set(std::slice::from_ref(&owner)).unwrap();
1357 let add_payload =
1358 inbox_set_delta(TlsSetMutation::Insert(InboxId::from_hex(&new_sa).unwrap()));
1359
1360 let new_bytes = apply_app_data_update_payload(
1361 ComponentId::SUPER_ADMIN_LIST,
1362 &add_payload,
1363 Some(&prior),
1364 &empty_registry(),
1365 )
1366 .unwrap();
1367
1368 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1369 assert_eq!(set.len(), 2);
1370 assert!(set.contains(&fake_inbox(0xAA)));
1371 assert!(set.contains(&fake_inbox(0xBB)));
1372 }
1373
1374 #[xmtp_common::test]
1375 fn test_apply_admin_list_malformed_payload_returns_tls_codec_error() {
1376 let err = apply_app_data_update_payload(
1383 ComponentId::ADMIN_LIST,
1384 &[0xff, 0xff, 0xff, 0xff],
1385 None,
1386 &empty_registry(),
1387 )
1388 .unwrap_err();
1389 assert!(
1390 matches!(err, ComponentSourceError::TlsCodec(_)),
1391 "got {err:?}"
1392 );
1393 }
1394
1395 #[xmtp_common::test]
1396 fn test_apply_admin_list_malformed_old_value_returns_tls_codec_error() {
1397 let inbox = fake_inbox_id(0x55);
1400 let payload = inbox_set_delta(TlsSetMutation::Insert(InboxId::from_hex(&inbox).unwrap()));
1401 let err = apply_app_data_update_payload(
1402 ComponentId::ADMIN_LIST,
1403 &payload,
1404 Some(&[0xde, 0xad, 0xbe, 0xef]),
1405 &empty_registry(),
1406 )
1407 .unwrap_err();
1408 assert!(
1409 matches!(err, ComponentSourceError::TlsCodec(_)),
1410 "got {err:?}"
1411 );
1412 }
1413
1414 #[xmtp_common::test]
1415 fn test_apply_immutable_first_insert_allowed() {
1416 let bytes = apply_app_data_update_payload(
1423 ComponentId::CONVERSATION_TYPE,
1424 b"seed",
1425 None,
1426 &empty_registry(),
1427 )
1428 .unwrap();
1429 assert_eq!(bytes, b"seed");
1430 }
1431
1432 #[xmtp_common::test]
1433 fn test_apply_immutable_overwrite_rejected() {
1434 let err = apply_app_data_update_payload(
1439 ComponentId::CONVERSATION_TYPE,
1440 b"junk",
1441 Some(b"prior"),
1442 &empty_registry(),
1443 )
1444 .unwrap_err();
1445 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1446 }
1447
1448 #[xmtp_common::test]
1449 fn test_apply_component_registry_delta_against_empty() {
1450 let id_a = ComponentId::GROUP_NAME;
1456 let id_b = ComponentId::GROUP_DESCRIPTION;
1457 let meta_a = registry_with(id_a, ComponentType::String)
1458 .get(&id_a)
1459 .unwrap()
1460 .unwrap()
1461 .encode_to_vec();
1462 let meta_b = registry_with(id_b, ComponentType::Bytes)
1463 .get(&id_b)
1464 .unwrap()
1465 .unwrap()
1466 .encode_to_vec();
1467 let delta = TlsMapDelta::<ComponentId, VLBytes>::new()
1468 .insert(id_a, VLBytes::new(meta_a.clone()))
1469 .insert(id_b, VLBytes::new(meta_b.clone()));
1470 let payload = delta.tls_serialize_detached().unwrap();
1471
1472 let new_bytes = apply_app_data_update_payload(
1473 ComponentId::COMPONENT_REGISTRY,
1474 &payload,
1475 None,
1476 &empty_registry(),
1477 )
1478 .unwrap();
1479 let map = TlsMap::<ComponentId, VLBytes>::tls_deserialize_exact(&new_bytes).unwrap();
1480 assert_eq!(map.len(), 2);
1481 assert_eq!(
1482 map.get(&id_a).map(|v| v.as_slice()),
1483 Some(meta_a.as_slice())
1484 );
1485 assert_eq!(
1486 map.get(&id_b).map(|v| v.as_slice()),
1487 Some(meta_b.as_slice())
1488 );
1489 }
1490
1491 #[xmtp_common::test]
1492 fn test_apply_group_membership_delta_against_existing_map() {
1493 let alice = fake_inbox(0xAA);
1497 let bob = fake_inbox(0xBB);
1498 let mut prior: TlsMap<InboxId, VLBytes> = TlsMap::new();
1499 prior.set(alice, VLBytes::new(vec![0x01]));
1500 prior.set(bob, VLBytes::new(vec![0x02]));
1501 let prior_bytes = prior.tls_serialize_detached().unwrap();
1502
1503 let delta = TlsMapDelta::<InboxId, VLBytes>::new()
1504 .update(alice, VLBytes::new(vec![0x99]))
1505 .delete(bob);
1506 let payload = delta.tls_serialize_detached().unwrap();
1507
1508 let new_bytes = apply_app_data_update_payload(
1509 ComponentId::GROUP_MEMBERSHIP,
1510 &payload,
1511 Some(&prior_bytes),
1512 &empty_registry(),
1513 )
1514 .unwrap();
1515 let map = TlsMap::<InboxId, VLBytes>::tls_deserialize_exact(&new_bytes).unwrap();
1516 assert_eq!(map.len(), 1);
1517 assert_eq!(
1518 map.get(&alice).map(|v| v.as_slice()),
1519 Some([0x99].as_slice())
1520 );
1521 assert!(!map.contains_key(&bob));
1522 }
1523
1524 #[xmtp_common::test]
1525 fn test_apply_map_component_malformed_delta_returns_codec_error() {
1526 let err = apply_app_data_update_payload(
1529 ComponentId::COMPONENT_REGISTRY,
1530 &[0xff, 0xff, 0xff, 0xff],
1531 None,
1532 &empty_registry(),
1533 )
1534 .unwrap_err();
1535 assert!(
1536 matches!(err, ComponentSourceError::TlsCodec(_)),
1537 "got {err:?}"
1538 );
1539 }
1540
1541 #[xmtp_common::test]
1542 fn test_apply_map_component_apply_failure_surfaces_apply_error() {
1543 let alice = fake_inbox(0x01);
1546 let delta = TlsMapDelta::<InboxId, VLBytes>::new().update(alice, VLBytes::new(vec![0x42]));
1547 let payload = delta.tls_serialize_detached().unwrap();
1548 let err = apply_app_data_update_payload(
1549 ComponentId::GROUP_MEMBERSHIP,
1550 &payload,
1551 None,
1552 &empty_registry(),
1553 )
1554 .unwrap_err();
1555 assert!(
1556 matches!(
1557 err,
1558 ComponentSourceError::TlsMapApply(TlsMapError::KeyNotFound)
1559 ),
1560 "got {err:?}"
1561 );
1562 }
1563
1564 #[xmtp_common::test]
1577 fn test_apply_unknown_id_with_bytes_registry_entry_stores_payload() {
1578 let id = ComponentId::new(0xC123);
1582 let registry = registry_with(id, ComponentType::Bytes);
1583 let new_value = apply_app_data_update_payload(id, b"opaque", None, ®istry).unwrap();
1584 assert_eq!(new_value, b"opaque");
1585 }
1586
1587 #[xmtp_common::test]
1588 fn test_apply_unknown_id_with_string_registry_entry_validates_utf8() {
1589 let id = ComponentId::new(0xC222);
1593 let registry = registry_with(id, ComponentType::String);
1594 let ok = apply_app_data_update_payload(id, b"hello", None, ®istry).unwrap();
1595 assert_eq!(ok, b"hello");
1596 let err = apply_app_data_update_payload(id, &[0xC3, 0x28], None, ®istry).unwrap_err();
1597 assert!(matches!(
1598 err,
1599 ComponentSourceError::MalformedComponentValue { .. }
1600 ));
1601 }
1602
1603 #[xmtp_common::test]
1604 fn test_apply_unknown_id_with_tls_set_inbox_id_applies_delta() {
1605 let id = ComponentId::new(0xC333);
1610 let registry = registry_with(id, ComponentType::TlsSetInboxId);
1611 let bob = fake_inbox(0x02);
1612 let delta = TlsSetDelta::<InboxId>::new().insert(bob);
1613 let payload = delta.tls_serialize_detached().unwrap();
1614 let new_bytes = apply_app_data_update_payload(id, &payload, None, ®istry).unwrap();
1615 let set = TlsSet::<InboxId>::tls_deserialize_exact(&new_bytes).unwrap();
1616 assert_eq!(set.len(), 1);
1617 assert!(set.contains(&bob));
1618 }
1619
1620 #[xmtp_common::test]
1621 fn test_apply_unknown_id_with_no_registry_entry_rejected() {
1622 let err =
1628 apply_app_data_update_payload(ComponentId::new(0xC456), b"x", None, &empty_registry())
1629 .unwrap_err();
1630 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1631 }
1632
1633 #[xmtp_common::test]
1642 fn test_apply_unknown_id_in_xmtp_immutable_range_first_write_allowed() {
1643 let id = ComponentId::new(0xBE05); assert!(id.is_immutable());
1645 let registry = registry_with(id, ComponentType::Bytes);
1646 let new_value = apply_app_data_update_payload(id, b"seed", None, ®istry).unwrap();
1647 assert_eq!(new_value, b"seed");
1648 }
1649
1650 #[xmtp_common::test]
1651 fn test_apply_unknown_id_in_xmtp_immutable_range_overwrite_rejected() {
1652 let id = ComponentId::new(0xBE05);
1653 assert!(id.is_immutable());
1654 let registry = registry_with(id, ComponentType::Bytes);
1655 let err = apply_app_data_update_payload(id, b"new", Some(b"old"), ®istry).unwrap_err();
1656 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1657 }
1658
1659 #[xmtp_common::test]
1664 fn test_apply_unknown_id_in_app_immutable_range_overwrite_rejected() {
1665 let id = ComponentId::new(0xFD42); assert!(id.is_immutable());
1667 let registry = registry_with(id, ComponentType::Bytes);
1668 let err = apply_app_data_update_payload(id, b"new", Some(b"old"), ®istry).unwrap_err();
1669 assert!(matches!(err, ComponentSourceError::ImmutableUpdate(_)));
1670 }
1671
1672 #[xmtp_common::test]
1673 fn test_apply_reserved_range_component_rejected_with_or_without_registry() {
1674 let err =
1679 apply_app_data_update_payload(ComponentId::new(0xFF01), b"x", None, &empty_registry())
1680 .unwrap_err();
1681 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1682 }
1683
1684 #[xmtp_common::test]
1685 fn test_apply_out_of_range_component_rejected() {
1686 let err =
1688 apply_app_data_update_payload(ComponentId::new(0x0042), b"x", None, &empty_registry())
1689 .unwrap_err();
1690 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1691 }
1692
1693 #[xmtp_common::test]
1694 fn test_apply_unknown_xmtp_range_id_with_registry_dispatches() {
1695 let id = ComponentId::new(0x8FFF);
1699 assert!(id.is_xmtp_range());
1700 let registry = registry_with(id, ComponentType::Bytes);
1701 let new_value = apply_app_data_update_payload(id, b"opaque", None, ®istry).unwrap();
1702 assert_eq!(new_value, b"opaque");
1703 }
1704
1705 fn update_op(delta: TlsSetDelta<InboxId>) -> AppDataUpdateOperation {
1710 AppDataUpdateOperation::Update(delta.tls_serialize_detached().unwrap().into())
1711 }
1712
1713 #[xmtp_common::test]
1714 fn test_expand_insert_surfaces_inbox_id_bytes() {
1715 let alice = fake_inbox(0x11);
1716 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1717 mutations: vec![TlsSetMutation::Insert(alice)],
1718 };
1719 let changes = expand_app_data_update_to_changes(
1720 ComponentId::ADMIN_LIST,
1721 &update_op(delta),
1722 None,
1723 &empty_registry(),
1724 )
1725 .unwrap();
1726 assert_eq!(changes.len(), 1);
1727 assert_eq!(changes[0].op, ComponentOp::Insert);
1728 assert_eq!(
1729 changes[0].value.as_deref(),
1730 Some(alice.as_bytes().as_slice())
1731 );
1732 }
1733
1734 #[xmtp_common::test]
1735 fn test_expand_remove_surfaces_inbox_id_bytes() {
1736 let alice = fake_inbox(0x22);
1737 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1738 mutations: vec![TlsSetMutation::Remove(alice)],
1739 };
1740 let changes = expand_app_data_update_to_changes(
1741 ComponentId::ADMIN_LIST,
1742 &update_op(delta),
1743 None,
1744 &empty_registry(),
1745 )
1746 .unwrap();
1747 assert_eq!(changes.len(), 1);
1748 assert_eq!(changes[0].op, ComponentOp::Delete);
1749 assert_eq!(
1750 changes[0].value.as_deref(),
1751 Some(alice.as_bytes().as_slice())
1752 );
1753 }
1754
1755 #[xmtp_common::test]
1756 fn test_expand_remove_by_hash_resolves_to_inbox_id_from_old_value() {
1757 let alice = fake_inbox(0xAA);
1761 let bob = fake_inbox(0xBB);
1762 let prior: TlsSet<InboxId> = [alice, bob].into_iter().collect();
1763 let old_bytes = prior.tls_serialize_detached().unwrap();
1764
1765 let hash = TlsKeyHash::of(&alice).unwrap();
1766 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1767 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1768 };
1769 let changes = expand_app_data_update_to_changes(
1770 ComponentId::ADMIN_LIST,
1771 &update_op(delta),
1772 Some(&old_bytes),
1773 &empty_registry(),
1774 )
1775 .unwrap();
1776 assert_eq!(changes.len(), 1);
1777 assert_eq!(changes[0].op, ComponentOp::Delete);
1778 assert_eq!(
1779 changes[0].value.as_deref(),
1780 Some(alice.as_bytes().as_slice())
1781 );
1782 }
1783
1784 #[xmtp_common::test]
1785 fn test_expand_remove_by_hash_miss_surfaces_none_value() {
1786 let alice = fake_inbox(0x01);
1790 let bob = fake_inbox(0x02);
1791 let prior: TlsSet<InboxId> = [alice].into_iter().collect();
1792 let old_bytes = prior.tls_serialize_detached().unwrap();
1793
1794 let hash = TlsKeyHash::of(&bob).unwrap();
1795 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1796 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1797 };
1798 let changes = expand_app_data_update_to_changes(
1799 ComponentId::ADMIN_LIST,
1800 &update_op(delta),
1801 Some(&old_bytes),
1802 &empty_registry(),
1803 )
1804 .unwrap();
1805 assert_eq!(changes.len(), 1);
1806 assert_eq!(changes[0].op, ComponentOp::Delete);
1807 assert!(changes[0].value.is_none());
1808 }
1809
1810 #[xmtp_common::test]
1811 fn test_expand_remove_by_hash_with_no_old_value_surfaces_none() {
1812 let hash = TlsKeyHash::of(&fake_inbox(0x33)).unwrap();
1814 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1815 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1816 };
1817 let changes = expand_app_data_update_to_changes(
1818 ComponentId::SUPER_ADMIN_LIST,
1819 &update_op(delta),
1820 None,
1821 &empty_registry(),
1822 )
1823 .unwrap();
1824 assert_eq!(changes.len(), 1);
1825 assert_eq!(changes[0].op, ComponentOp::Delete);
1826 assert!(changes[0].value.is_none());
1827 }
1828
1829 #[xmtp_common::test]
1830 fn test_expand_remove_by_hash_malformed_old_value_surfaces_codec_error() {
1831 let hash = TlsKeyHash::of(&fake_inbox(0x44)).unwrap();
1835 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1836 mutations: vec![TlsSetMutation::RemoveByHash(hash)],
1837 };
1838 let err = expand_app_data_update_to_changes(
1839 ComponentId::ADMIN_LIST,
1840 &update_op(delta),
1841 Some(&[0xde, 0xad, 0xbe, 0xef]),
1842 &empty_registry(),
1843 )
1844 .unwrap_err();
1845 assert!(
1846 matches!(err, ComponentSourceError::TlsCodec(_)),
1847 "got {err:?}"
1848 );
1849 }
1850
1851 #[xmtp_common::test]
1856 fn test_expand_unknown_component_bytes_typed_emits_single_update() {
1857 let id = ComponentId::new(0x80FF);
1858 let registry = registry_with(id, ComponentType::Bytes);
1859 let changes = expand_app_data_update_to_changes(
1860 id,
1861 &AppDataUpdateOperation::Update(b"opaque".to_vec().into()),
1862 None,
1863 ®istry,
1864 )
1865 .unwrap();
1866 assert_eq!(changes.len(), 1);
1867 assert_eq!(changes[0].op, ComponentOp::Update);
1868 assert_eq!(changes[0].value.as_deref(), Some(b"opaque".as_slice()));
1869 }
1870
1871 #[xmtp_common::test]
1874 fn test_expand_unknown_component_remove_emits_single_delete() {
1875 let id = ComponentId::new(0x80FF);
1876 let registry = registry_with(id, ComponentType::Bytes);
1877 let changes =
1878 expand_app_data_update_to_changes(id, &AppDataUpdateOperation::Remove, None, ®istry)
1879 .unwrap();
1880 assert_eq!(changes.len(), 1);
1881 assert_eq!(changes[0].op, ComponentOp::Delete);
1882 assert!(changes[0].value.is_none());
1883 }
1884
1885 #[xmtp_common::test]
1891 fn test_expand_unknown_component_tls_set_inbox_id_emits_per_element_changes() {
1892 let id = ComponentId::new(0x80FE);
1893 let registry = registry_with(id, ComponentType::TlsSetInboxId);
1894 let alice = fake_inbox(0x11);
1895 let bob = fake_inbox(0x22);
1896 let delta = TlsSetDelta::<InboxId>::new().insert(alice).insert(bob);
1897 let payload = delta.tls_serialize_detached().unwrap();
1898 let changes = expand_app_data_update_to_changes(
1899 id,
1900 &AppDataUpdateOperation::Update(payload.into()),
1901 None,
1902 ®istry,
1903 )
1904 .unwrap();
1905 assert_eq!(changes.len(), 2);
1906 assert!(changes.iter().all(|c| c.op == ComponentOp::Insert));
1907 }
1908
1909 #[xmtp_common::test]
1914 fn test_expand_reserved_range_id_still_rejected() {
1915 let err = expand_app_data_update_to_changes(
1916 ComponentId::new(0xFF02),
1917 &AppDataUpdateOperation::Update(b"x".to_vec().into()),
1918 None,
1919 &empty_registry(),
1920 )
1921 .unwrap_err();
1922 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1923 }
1924
1925 #[xmtp_common::test]
1930 fn test_expand_unknown_component_no_registry_entry_rejected() {
1931 let err = expand_app_data_update_to_changes(
1932 ComponentId::new(0x80FF),
1933 &AppDataUpdateOperation::Update(b"opaque".to_vec().into()),
1934 None,
1935 &empty_registry(),
1936 )
1937 .unwrap_err();
1938 assert!(matches!(err, ComponentSourceError::UnknownComponent(_)));
1939 }
1940
1941 #[xmtp_common::test]
1942 fn test_expand_skips_old_value_decode_when_no_remove_by_hash() {
1943 let alice = fake_inbox(0x55);
1946 let delta: TlsSetDelta<InboxId> = TlsSetDelta {
1947 mutations: vec![TlsSetMutation::Insert(alice)],
1948 };
1949 let changes = expand_app_data_update_to_changes(
1950 ComponentId::ADMIN_LIST,
1951 &update_op(delta),
1952 Some(&[0xff, 0xff, 0xff]), &empty_registry(),
1954 )
1955 .unwrap();
1956 assert_eq!(changes.len(), 1);
1957 assert_eq!(changes[0].op, ComponentOp::Insert);
1958 }
1959
1960 use openmls::extensions::{
1969 AppDataDictionary, AppDataDictionaryExtension, Extension as OpenMlsExtension, Extensions,
1970 };
1971
1972 fn extensions_with_entries(
1974 entries: &[(u16, Vec<u8>)],
1975 ) -> Extensions<openmls::group::GroupContext> {
1976 let mut dict = AppDataDictionary::new();
1977 for (id, bytes) in entries {
1978 let _ = dict.insert(*id, bytes.clone());
1979 }
1980 Extensions::from_vec(vec![OpenMlsExtension::AppDataDictionary(
1981 AppDataDictionaryExtension::new(dict),
1982 )])
1983 .expect("valid group-context extension set")
1984 }
1985
1986 #[xmtp_common::test]
1989 fn read_super_admin_list_absent_returns_none() {
1990 let exts = extensions_with_entries(&[]);
1993 assert!(
1994 read_super_admin_list_from_extensions(&exts)
1995 .unwrap()
1996 .is_none()
1997 );
1998 }
1999
2000 #[xmtp_common::test]
2001 fn read_super_admin_list_happy_path() {
2002 let ids = vec![fake_inbox_id(0xAA), fake_inbox_id(0xBB)];
2003 let bytes = encode_inbox_id_set(&ids).unwrap();
2004 let exts = extensions_with_entries(&[(ComponentId::SUPER_ADMIN_LIST.as_u16(), bytes)]);
2005 let got = read_super_admin_list_from_extensions(&exts)
2006 .unwrap()
2007 .unwrap();
2008 assert_eq!(got.len(), 2);
2009 assert_eq!(got[0], fake_inbox_id(0xAA));
2011 assert_eq!(got[1], fake_inbox_id(0xBB));
2012 }
2013
2014 #[xmtp_common::test]
2015 fn read_super_admin_list_malformed_bytes_surface_error() {
2016 let exts = extensions_with_entries(&[(
2017 ComponentId::SUPER_ADMIN_LIST.as_u16(),
2018 vec![0x00, 0xDE, 0xAD],
2019 )]);
2020 let err = read_super_admin_list_from_extensions(&exts).unwrap_err();
2021 assert!(matches!(
2022 err,
2023 ComponentSourceError::MalformedComponentValue {
2024 component_id,
2025 ..
2026 } if component_id == ComponentId::SUPER_ADMIN_LIST
2027 ));
2028 }
2029
2030 fn encode_conv_type_bytes(value: i32) -> Vec<u8> {
2033 value.to_be_bytes().to_vec()
2034 }
2035
2036 fn encode_dm_pair(tag_a: u8, tag_b: u8) -> Vec<u8> {
2037 encode_inbox_id_set(&[fake_inbox_id(tag_a), fake_inbox_id(tag_b)]).unwrap()
2038 }
2039
2040 fn encode_creator_bytes(tag: u8) -> Vec<u8> {
2043 fake_inbox(tag).tls_serialize_detached().unwrap()
2044 }
2045
2046 #[xmtp_common::test]
2047 fn read_group_metadata_missing_required_seeds_returns_none() {
2048 let exts = extensions_with_entries(&[]);
2050 assert!(
2051 read_group_metadata_from_extensions(&exts)
2052 .unwrap()
2053 .is_none()
2054 );
2055 }
2056
2057 #[xmtp_common::test]
2058 fn read_group_metadata_happy_path_non_dm() {
2059 let exts = extensions_with_entries(&[
2060 (
2061 ComponentId::CONVERSATION_TYPE.as_u16(),
2062 encode_conv_type_bytes(1),
2063 ),
2064 (
2065 ComponentId::CREATOR_INBOX_ID.as_u16(),
2066 encode_creator_bytes(0x11),
2067 ),
2068 ]);
2069 let got = read_group_metadata_from_extensions(&exts).unwrap().unwrap();
2070 assert_eq!(got.conversation_type, 1);
2071 assert_eq!(got.creator_inbox_id, fake_inbox_id(0x11));
2072 assert!(got.dm_members.is_none());
2073 assert!(got.oneshot.is_none());
2074 }
2075
2076 #[xmtp_common::test]
2077 fn read_group_metadata_dm_happy_path() {
2078 let exts = extensions_with_entries(&[
2081 (
2082 ComponentId::CONVERSATION_TYPE.as_u16(),
2083 encode_conv_type_bytes(2),
2084 ),
2085 (
2086 ComponentId::CREATOR_INBOX_ID.as_u16(),
2087 encode_creator_bytes(0x22),
2088 ),
2089 (ComponentId::DM_MEMBERS.as_u16(), encode_dm_pair(0x22, 0x33)),
2090 ]);
2091 let got = read_group_metadata_from_extensions(&exts).unwrap().unwrap();
2092 let dm = got.dm_members.unwrap();
2093 assert_eq!(dm.dm_member_one.unwrap().inbox_id, fake_inbox_id(0x22));
2094 assert_eq!(dm.dm_member_two.unwrap().inbox_id, fake_inbox_id(0x33));
2095 }
2096
2097 #[xmtp_common::test]
2098 fn read_group_metadata_dm_wrong_cardinality_errors() {
2099 let one_element = encode_inbox_id_set(&[fake_inbox_id(0x44)]).unwrap();
2102 let exts = extensions_with_entries(&[
2103 (
2104 ComponentId::CONVERSATION_TYPE.as_u16(),
2105 encode_conv_type_bytes(2),
2106 ),
2107 (
2108 ComponentId::CREATOR_INBOX_ID.as_u16(),
2109 encode_creator_bytes(0x44),
2110 ),
2111 (ComponentId::DM_MEMBERS.as_u16(), one_element),
2112 ]);
2113 let err = read_group_metadata_from_extensions(&exts).unwrap_err();
2114 assert!(matches!(
2115 err,
2116 ComponentSourceError::MalformedComponentValue {
2117 component_id,
2118 ..
2119 } if component_id == ComponentId::DM_MEMBERS
2120 ));
2121 }
2122
2123 #[xmtp_common::test(unwrap_try = true)]
2129 fn read_group_metadata_rejects_dm_members_outside_a_dm() {
2130 let read = |conversation_type: i32| {
2131 read_group_metadata_from_extensions(&extensions_with_entries(&[
2132 (
2133 ComponentId::CONVERSATION_TYPE.as_u16(),
2134 encode_conv_type_bytes(conversation_type),
2135 ),
2136 (
2137 ComponentId::CREATOR_INBOX_ID.as_u16(),
2138 encode_creator_bytes(0x22),
2139 ),
2140 (ComponentId::DM_MEMBERS.as_u16(), encode_dm_pair(0x22, 0x33)),
2141 ]))
2142 };
2143 read(2)?;
2144 for non_dm in [0, 1, 3, 4] {
2145 assert!(matches!(
2146 read(non_dm),
2147 Err(ComponentSourceError::MalformedComponentValue { component_id, .. })
2148 if component_id == ComponentId::DM_MEMBERS
2149 ));
2150 }
2151 }
2152
2153 #[xmtp_common::test]
2154 fn read_group_metadata_malformed_creator_errors() {
2155 let exts = extensions_with_entries(&[
2161 (
2162 ComponentId::CONVERSATION_TYPE.as_u16(),
2163 encode_conv_type_bytes(1),
2164 ),
2165 (
2166 ComponentId::CREATOR_INBOX_ID.as_u16(),
2167 vec![0xFF, 0xFE, 0xFD],
2168 ),
2169 ]);
2170 let err = read_group_metadata_from_extensions(&exts).unwrap_err();
2171 assert!(matches!(
2172 err,
2173 ComponentSourceError::MalformedComponentValue {
2174 component_id,
2175 ..
2176 } if component_id == ComponentId::CREATOR_INBOX_ID
2177 ));
2178 }
2179
2180 #[xmtp_common::test]
2183 fn read_group_membership_happy_path_flattens_per_inbox() {
2184 use crate::app_data::migration::encode_group_membership_dict;
2185 use std::collections::BTreeMap;
2186 use xmtp_proto::xmtp::mls::message_contents::{
2187 GroupMembershipEntry,
2188 group_membership_entry::{V1 as GroupMembershipEntryV1, Version},
2189 };
2190 let mut entries: BTreeMap<InboxId, GroupMembershipEntry> = BTreeMap::new();
2191 entries.insert(
2192 InboxId::from_bytes([0x11; INBOX_ID_BYTE_LEN]),
2193 GroupMembershipEntry {
2194 version: Some(Version::V1(GroupMembershipEntryV1 {
2195 sequence_id: 7,
2196 failed_installations: vec![vec![0xA1; 16]],
2197 })),
2198 },
2199 );
2200 entries.insert(
2201 InboxId::from_bytes([0x22; INBOX_ID_BYTE_LEN]),
2202 GroupMembershipEntry {
2203 version: Some(Version::V1(GroupMembershipEntryV1 {
2204 sequence_id: 42,
2205 failed_installations: vec![vec![0xB1; 16]],
2206 })),
2207 },
2208 );
2209 let bytes = encode_group_membership_dict(&entries).unwrap();
2210 let exts = extensions_with_entries(&[(ComponentId::GROUP_MEMBERSHIP.as_u16(), bytes)]);
2211
2212 let proto = read_group_membership_from_dict(&exts).unwrap().unwrap();
2213 assert_eq!(proto.members.len(), 2);
2215 assert_eq!(proto.members.get(&fake_inbox_id(0x11)), Some(&7));
2216 assert_eq!(proto.members.get(&fake_inbox_id(0x22)), Some(&42));
2217 assert_eq!(proto.failed_installations.len(), 2);
2219 }
2220
2221 #[xmtp_common::test]
2222 fn read_group_membership_malformed_bytes_surface_error() {
2223 let exts = extensions_with_entries(&[(
2224 ComponentId::GROUP_MEMBERSHIP.as_u16(),
2225 vec![0xDE, 0xAD, 0xBE, 0xEF],
2226 )]);
2227 let err = read_group_membership_from_dict(&exts).unwrap_err();
2228 assert!(matches!(
2229 err,
2230 ComponentSourceError::MalformedComponentValue {
2231 component_id,
2232 ..
2233 } if component_id == ComponentId::GROUP_MEMBERSHIP
2234 ));
2235 }
2236
2237 use crate::group_mutable_metadata::{GroupMutableMetadata, MetadataField};
2252
2253 fn empty_base_gmm() -> GroupMutableMetadata {
2254 GroupMutableMetadata::new(std::collections::HashMap::new(), Vec::new(), Vec::new())
2255 }
2256
2257 #[xmtp_common::test]
2258 fn merge_with_malformed_registry_returns_valid_field() {
2259 let exts = extensions_with_entries(&[
2265 (ComponentId::COMPONENT_REGISTRY.as_u16(), vec![0xCA; 4]),
2266 (ComponentId::GROUP_NAME.as_u16(), b"My Group".to_vec()),
2267 ]);
2268 let mut base = empty_base_gmm();
2269 merge_app_data_into_mutable_metadata_from_extensions(&mut base, &exts)
2270 .expect("merge ignores registry parseability and reads field directly");
2271 assert_eq!(
2272 base.attributes
2273 .get(MetadataField::GroupName.as_str())
2274 .map(String::as_str),
2275 Some("My Group"),
2276 );
2277 }
2278
2279 #[xmtp_common::test]
2280 fn merge_with_malformed_field_surfaces_error_not_silent_loss() {
2281 let exts =
2287 extensions_with_entries(&[(ComponentId::ADMIN_LIST.as_u16(), vec![0xff, 0xff, 0xff])]);
2288 let mut base = empty_base_gmm();
2289 let err =
2290 merge_app_data_into_mutable_metadata_from_extensions(&mut base, &exts).unwrap_err();
2291 assert!(
2292 matches!(
2293 err,
2294 ComponentSourceError::MalformedComponentValue { component_id, .. }
2295 if component_id == ComponentId::ADMIN_LIST
2296 ),
2297 "expected MalformedComponentValue for ADMIN_LIST, got: {err:?}"
2298 );
2299 }
2300
2301 fn bytes(len: usize) -> VLBytes {
2304 VLBytes::new(vec![b'a'; len])
2305 }
2306
2307 #[xmtp_common::test(unwrap_try = true)]
2311 fn bounded_scalar_holds_at_most_the_element_limit() {
2312 let app = ComponentId::new(0xC068);
2313 let registry = registry_with(app, ComponentType::Bytes);
2314 for id in [app, ComponentId::GROUP_IMAGE] {
2315 let at_limit = vec![0; MAX_FIELD_ELEMENT_BYTES];
2316 assert_eq!(
2317 apply_app_data_update_payload(id, &at_limit, None, ®istry)?,
2318 at_limit
2319 );
2320 let over = [0; MAX_FIELD_ELEMENT_BYTES + 1];
2321 assert!(matches!(
2322 apply_app_data_update_payload(id, &over, None, ®istry),
2323 Err(ComponentSourceError::FieldBoundExceeded {
2324 component_id,
2325 len: 8193,
2326 max: MAX_FIELD_ELEMENT_BYTES,
2327 }) if component_id == id
2328 ));
2329 }
2330 }
2331
2332 #[xmtp_common::test(unwrap_try = true)]
2336 fn bounded_map_elements_hold_at_most_the_element_limit() {
2337 let app = ComponentId::new(0xC069);
2338 let registry = registry_with(app, ComponentType::TlsMapBytesBytes);
2339 let app_entry = |key: usize, value: usize| {
2340 let delta = TlsMapDelta::<VLBytes, VLBytes>::new().insert(bytes(key), bytes(value));
2341 apply_app_data_update_payload(
2342 app,
2343 &delta.tls_serialize_detached().unwrap(),
2344 None,
2345 ®istry,
2346 )
2347 };
2348 let name = |len: usize| {
2349 let delta = TlsMapDelta::<InboxId, VLBytes>::new().insert(fake_inbox(1), bytes(len));
2350 apply_app_data_update_payload(
2351 ComponentId::USER_DISPLAY_NAME,
2352 &delta.tls_serialize_detached().unwrap(),
2353 None,
2354 ®istry,
2355 )
2356 };
2357 let over = |result| {
2358 matches!(
2359 result,
2360 Err(ComponentSourceError::FieldBoundExceeded {
2361 len: 8193,
2362 max: MAX_FIELD_ELEMENT_BYTES,
2363 ..
2364 })
2365 )
2366 };
2367
2368 app_entry(MAX_FIELD_ELEMENT_BYTES, MAX_FIELD_ELEMENT_BYTES)?;
2369 assert!(over(app_entry(MAX_FIELD_ELEMENT_BYTES + 1, 1)));
2370 assert!(over(app_entry(1, MAX_FIELD_ELEMENT_BYTES + 1)));
2371 name(MAX_FIELD_ELEMENT_BYTES)?;
2372 assert!(over(name(MAX_FIELD_ELEMENT_BYTES + 1)));
2373 }
2374
2375 #[xmtp_common::test(unwrap_try = true)]
2379 fn bounded_collection_holds_at_most_the_snapshot_limit() {
2380 let app = ComponentId::new(0xC06A);
2381 let registry = registry_with(app, ComponentType::TlsMapBytesBytes);
2382 let write = |last: usize| {
2384 let delta = (0..8u8)
2385 .fold(TlsMapDelta::<VLBytes, VLBytes>::new(), |d, k| {
2386 d.insert(VLBytes::new(vec![k]), bytes(8000))
2387 })
2388 .insert(VLBytes::new(vec![8]), bytes(last));
2389 apply_app_data_update_payload(
2390 app,
2391 &delta.tls_serialize_detached().unwrap(),
2392 None,
2393 ®istry,
2394 )
2395 };
2396 let last = 64 + MAX_FIELD_SNAPSHOT_BYTES - write(64)?.len();
2399
2400 assert_eq!(write(last)?.len(), MAX_FIELD_SNAPSHOT_BYTES);
2401 assert!(matches!(
2402 write(last + 1),
2403 Err(ComponentSourceError::FieldBoundExceeded {
2404 len: 65537,
2405 max: MAX_FIELD_SNAPSHOT_BYTES,
2406 ..
2407 })
2408 ));
2409 }
2410
2411 #[xmtp_common::test(unwrap_try = true)]
2415 fn apply_bounds_elements_that_the_payload_later_removes() {
2416 let set = ComponentId::new(0xC06D);
2417 let map = ComponentId::new(0xC06E);
2418 let over = || bytes(MAX_FIELD_ELEMENT_BYTES + 1);
2419 let apply = |id, payload: Vec<u8>, ty| {
2420 apply_app_data_update_payload(id, &payload, None, ®istry_with(id, ty))
2421 };
2422 let exceeded = |result| {
2423 matches!(
2424 result,
2425 Err(ComponentSourceError::FieldBoundExceeded { len: 8193, .. })
2426 )
2427 };
2428
2429 let set_payload = TlsSetDelta::<VLBytes>::new()
2430 .insert(over())
2431 .remove(over())
2432 .tls_serialize_detached()?;
2433 assert!(exceeded(apply(
2434 set,
2435 set_payload,
2436 ComponentType::TlsSetBytes
2437 )));
2438 let value_payload = TlsMapDelta::<VLBytes, VLBytes>::new()
2439 .insert(bytes(1), over())
2440 .delete(bytes(1))
2441 .tls_serialize_detached()?;
2442 assert!(exceeded(apply(
2443 map,
2444 value_payload,
2445 ComponentType::TlsMapBytesBytes
2446 )));
2447 let key_payload = TlsMapDelta::<VLBytes, VLBytes>::new()
2448 .insert(over(), bytes(1))
2449 .delete(over())
2450 .tls_serialize_detached()?;
2451 assert!(exceeded(apply(
2452 map,
2453 key_payload,
2454 ComponentType::TlsMapBytesBytes
2455 )));
2456 }
2457
2458 fn padded_registry_entry(len: usize) -> VLBytes {
2461 let id = ComponentId::new(0xC100);
2462 let mut entry = registry_with(id, ComponentType::Bytes)
2463 .get(&id)
2464 .unwrap()
2465 .unwrap()
2466 .encode_to_vec();
2467 let pad = len - entry.len() - 3;
2469 assert!((128..16384).contains(&pad));
2470 entry.extend([0x7A, pad as u8 | 0x80, (pad >> 7) as u8]);
2471 entry.resize(len, 0);
2472 VLBytes::new(entry)
2473 }
2474
2475 #[xmtp_common::test(unwrap_try = true)]
2480 fn registry_entries_hold_at_most_the_element_limit() {
2481 let registry = ComponentRegistry::new();
2482 let insert = |len| {
2483 TlsMapDelta::<ComponentId, VLBytes>::new()
2484 .insert(ComponentId::new(0xC100), padded_registry_entry(len))
2485 .tls_serialize_detached()
2486 .unwrap()
2487 };
2488 let apply = |payload: &[u8]| {
2489 apply_app_data_update_payload(ComponentId::COMPONENT_REGISTRY, payload, None, ®istry)
2490 .map(drop)
2491 };
2492 let check = |payload: &[u8]| {
2493 check_update_payload_bounds(ComponentId::COMPONENT_REGISTRY, payload, ®istry)
2494 };
2495 let exceeded = |result| {
2496 matches!(
2497 result,
2498 Err(ComponentSourceError::FieldBoundExceeded {
2499 len: 8193,
2500 max: MAX_FIELD_ELEMENT_BYTES,
2501 ..
2502 })
2503 )
2504 };
2505
2506 let at_limit = insert(MAX_FIELD_ELEMENT_BYTES);
2507 check(&at_limit)?;
2508 apply(&at_limit)?;
2509 let over = insert(MAX_FIELD_ELEMENT_BYTES + 1);
2510 assert!(exceeded(check(&over)));
2511 assert!(exceeded(apply(&over)));
2512 }
2513
2514 #[xmtp_common::test(unwrap_try = true)]
2519 fn registry_holds_at_most_the_snapshot_limit() {
2520 let write = |last: usize| {
2522 let delta = (0..8)
2523 .fold(TlsMapDelta::<ComponentId, VLBytes>::new(), |d, k| {
2524 d.insert(ComponentId::new(0xC100 + k), padded_registry_entry(8000))
2525 })
2526 .insert(ComponentId::new(0xC108), padded_registry_entry(last));
2527 apply_app_data_update_payload(
2528 ComponentId::COMPONENT_REGISTRY,
2529 &delta.tls_serialize_detached().unwrap(),
2530 None,
2531 &ComponentRegistry::new(),
2532 )
2533 };
2534 let last = 256 + MAX_FIELD_SNAPSHOT_BYTES - write(256)?.len();
2537
2538 assert_eq!(write(last)?.len(), MAX_FIELD_SNAPSHOT_BYTES);
2539 assert!(matches!(
2540 write(last + 1),
2541 Err(ComponentSourceError::FieldBoundExceeded {
2542 component_id: ComponentId::COMPONENT_REGISTRY,
2543 len: 65537,
2544 max: MAX_FIELD_SNAPSHOT_BYTES,
2545 })
2546 ));
2547 }
2548
2549 #[xmtp_common::test(unwrap_try = true)]
2554 fn field_bounds_skip_other_xmtp_components() {
2555 let registered = ComponentId::new(0x80FF);
2556 let over = [b'a'; MAX_FIELD_ELEMENT_BYTES + 1];
2557 let registry = registry_with(registered, ComponentType::Bytes);
2558 apply_app_data_update_payload(registered, &over, None, ®istry)?;
2559 apply_app_data_update_payload(ComponentId::GROUP_DESCRIPTION, &over, None, ®istry)?;
2560 }
2561
2562 #[xmtp_common::test(unwrap_try = true)]
2567 fn payload_bounds_measure_every_named_element() {
2568 let set = ComponentId::new(0xC06B);
2569 let map = ComponentId::new(0xC06C);
2570 let registries = [
2571 registry_with(set, ComponentType::TlsSetBytes),
2572 registry_with(map, ComponentType::TlsMapBytesBytes),
2573 ];
2574 let over = MAX_FIELD_ELEMENT_BYTES + 1;
2575 let check = |id, payload: Vec<u8>| {
2576 check_update_payload_bounds(id, &payload, ®istries[usize::from(id == map)])
2577 };
2578 let exceeded = |result| matches!(result, Err(ComponentSourceError::FieldBoundExceeded { len, .. }) if len == over);
2579
2580 let set_delta = |delta: TlsSetDelta<VLBytes>| delta.tls_serialize_detached().unwrap();
2581 check(
2582 set,
2583 set_delta(TlsSetDelta::new().insert(bytes(MAX_FIELD_ELEMENT_BYTES))),
2584 )?;
2585 assert!(exceeded(check(
2586 set,
2587 set_delta(TlsSetDelta::new().insert(bytes(over)))
2588 )));
2589 assert!(exceeded(check(
2590 set,
2591 set_delta(TlsSetDelta::new().remove(bytes(over)))
2592 )));
2593 check(
2594 set,
2595 set_delta(TlsSetDelta::new().remove(bytes(MAX_FIELD_ELEMENT_BYTES))),
2596 )?;
2597
2598 let map_delta =
2599 |delta: TlsMapDelta<VLBytes, VLBytes>| delta.tls_serialize_detached().unwrap();
2600 assert!(exceeded(check(
2601 map,
2602 map_delta(TlsMapDelta::new().update(bytes(over), bytes(1)))
2603 )));
2604 assert!(exceeded(check(
2605 map,
2606 map_delta(TlsMapDelta::new().update(bytes(1), bytes(over)))
2607 )));
2608 assert!(exceeded(check(
2609 map,
2610 map_delta(TlsMapDelta::new().delete(bytes(over)))
2611 )));
2612 check(
2613 map,
2614 map_delta(TlsMapDelta::new().delete(bytes(MAX_FIELD_ELEMENT_BYTES))),
2615 )?;
2616
2617 check(ComponentId::GROUP_DESCRIPTION, vec![b'a'; over])?;
2618 }
2619}