1use openmls::messages::proposals::AppDataUpdateOperation;
27use xmtp_cryptography::{Secret, configuration::ED25519_KEY_LENGTH};
28use xmtp_proto::xmtp::mls::message_contents::ComponentType;
29
30use crate::app_data::{
31 component_id::ComponentId,
32 component_registry::ComponentOp,
33 typed::{Component, ComponentInvariantError, ComponentTypedError, ExpandedComponentChange},
34};
35
36pub const MAX_GROUP_NAME_LENGTH: usize = 100;
38pub const MAX_GROUP_DESCRIPTION_LENGTH: usize = 1000;
40pub const MAX_GROUP_IMAGE_URL_LENGTH: usize = 2048;
42pub const MAX_APP_DATA_LENGTH: usize = 8192;
44
45pub(crate) fn apply_passthrough(payload: &[u8]) -> Result<Vec<u8>, ComponentTypedError> {
48 Ok(payload.to_vec())
49}
50
51pub(crate) fn expand_passthrough(
55 op: &AppDataUpdateOperation,
56) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
57 match op {
58 AppDataUpdateOperation::Update(payload) => Ok(vec![ExpandedComponentChange {
59 op: ComponentOp::Update,
60 key: None,
61 value: Some(payload.as_slice().to_vec()),
62 }]),
63 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
64 op: ComponentOp::Delete,
65 key: None,
66 value: None,
67 }]),
68 }
69}
70
71pub(crate) fn decode_utf8(
75 component_id: ComponentId,
76 bytes: &[u8],
77) -> Result<String, ComponentTypedError> {
78 std::str::from_utf8(bytes)
79 .map(str::to_owned)
80 .map_err(|err| ComponentTypedError::MalformedValue {
81 component_id,
82 reason: format!("not valid UTF-8: {err}"),
83 })
84}
85
86fn decode_be_i64(component_id: ComponentId, bytes: &[u8]) -> Result<i64, ComponentTypedError> {
89 let arr: [u8; 8] = bytes
90 .try_into()
91 .map_err(|_| ComponentTypedError::MalformedValue {
92 component_id,
93 reason: format!("expected 8 bytes (big-endian i64), got {}", bytes.len()),
94 })?;
95 Ok(i64::from_be_bytes(arr))
96}
97
98fn require_exact_len(
101 component_id: ComponentId,
102 bytes: &[u8],
103 expected: usize,
104) -> Result<(), ComponentTypedError> {
105 if bytes.len() != expected {
106 return Err(ComponentTypedError::MalformedValue {
107 component_id,
108 reason: format!("expected {} bytes, got {}", expected, bytes.len()),
109 });
110 }
111 Ok(())
112}
113
114macro_rules! passthrough_string_component {
125 ($struct_name:ident, $id:expr $(, $max_len:expr)?) => {
126 pub struct $struct_name;
127
128 impl Component for $struct_name {
129 const ID: ComponentId = $id;
130 const COMPONENT_TYPE: ComponentType = ComponentType::String;
131 type Value = String;
132 type Mutation = String;
133
134 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
135 decode_utf8(Self::ID, bytes)
136 }
137
138 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
139 Ok(value.as_bytes().to_vec())
140 }
141
142 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
143 Ok(mutation.as_bytes().to_vec())
144 }
145
146 fn apply_update_payload(
147 payload: &[u8],
148 _prior: Option<&[u8]>,
149 ) -> Result<Vec<u8>, ComponentTypedError> {
150 let _ = decode_utf8(Self::ID, payload)?;
161 apply_passthrough(payload)
162 }
163
164 fn expand_to_changes(
165 op: &AppDataUpdateOperation,
166 _prior: Option<&[u8]>,
167 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
168 if let AppDataUpdateOperation::Update(payload) = op {
170 let _ = decode_utf8(Self::ID, payload.as_slice())?;
171 }
172 expand_passthrough(op)
173 }
174
175 fn validate_invariant(
176 _change: &crate::app_data::validation::ComponentChange<'_>,
177 _registry: &crate::app_data::component_registry::ComponentRegistry,
178 ) -> Result<(), ComponentInvariantError> {
179 $(
180 if let Some(value) = _change.new_value
184 && value.len() > $max_len
185 {
186 return Err(ComponentInvariantError::Violation {
187 component_id: Self::ID,
188 reason: format!("value exceeds {} bytes", $max_len),
189 });
190 }
191 )?
192 Ok(())
193 }
194 }
195 };
196}
197
198macro_rules! be_i64_component {
201 ($struct_name:ident, $id:expr) => {
202 pub struct $struct_name;
203
204 impl Component for $struct_name {
205 const ID: ComponentId = $id;
206 const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
207 type Value = i64;
208 type Mutation = i64;
209
210 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
211 decode_be_i64(Self::ID, bytes)
212 }
213
214 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
215 Ok(value.to_be_bytes().to_vec())
216 }
217
218 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
219 Ok(mutation.to_be_bytes().to_vec())
220 }
221
222 fn apply_update_payload(
223 payload: &[u8],
224 _prior: Option<&[u8]>,
225 ) -> Result<Vec<u8>, ComponentTypedError> {
226 let _ = decode_be_i64(Self::ID, payload)?;
228 Ok(payload.to_vec())
229 }
230
231 fn expand_to_changes(
232 op: &AppDataUpdateOperation,
233 _prior: Option<&[u8]>,
234 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
235 match op {
236 AppDataUpdateOperation::Update(payload) => {
237 let _ = decode_be_i64(Self::ID, payload.as_slice())?;
238 Ok(vec![ExpandedComponentChange {
239 op: ComponentOp::Update,
240 key: None,
241 value: Some(payload.as_slice().to_vec()),
242 }])
243 }
244 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
245 op: ComponentOp::Delete,
246 key: None,
247 value: None,
248 }]),
249 }
250 }
251 }
252 };
253}
254
255passthrough_string_component!(
256 GroupNameComponent,
257 ComponentId::GROUP_NAME,
258 MAX_GROUP_NAME_LENGTH
259);
260passthrough_string_component!(
261 GroupDescriptionComponent,
262 ComponentId::GROUP_DESCRIPTION,
263 MAX_GROUP_DESCRIPTION_LENGTH
264);
265passthrough_string_component!(
266 GroupImageUrlComponent,
267 ComponentId::GROUP_IMAGE_URL,
268 MAX_GROUP_IMAGE_URL_LENGTH
269);
270passthrough_string_component!(AppDataComponent, ComponentId::APP_DATA, MAX_APP_DATA_LENGTH);
271passthrough_string_component!(
272 MinSupportedProtocolVersionComponent,
273 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
274);
275
276be_i64_component!(
277 MessageDisappearFromNsComponent,
278 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
279);
280be_i64_component!(
281 MessageDisappearInNsComponent,
282 ComponentId::MESSAGE_DISAPPEAR_IN_NS
283);
284
285pub struct CommitLogSignerComponent;
292
293impl Component for CommitLogSignerComponent {
294 const ID: ComponentId = ComponentId::COMMIT_LOG_SIGNER;
295 const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
296 type Value = Secret;
297 type Mutation = Secret;
298
299 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
300 require_exact_len(Self::ID, bytes, ED25519_KEY_LENGTH)?;
301 Ok(Secret::new(bytes.to_vec()))
302 }
303
304 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
305 require_exact_len(Self::ID, value.as_slice(), ED25519_KEY_LENGTH)?;
306 Ok(value.as_slice().to_vec())
307 }
308
309 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
310 require_exact_len(Self::ID, mutation.as_slice(), ED25519_KEY_LENGTH)?;
311 Ok(mutation.as_slice().to_vec())
312 }
313
314 fn apply_update_payload(
315 payload: &[u8],
316 _prior: Option<&[u8]>,
317 ) -> Result<Vec<u8>, ComponentTypedError> {
318 require_exact_len(Self::ID, payload, ED25519_KEY_LENGTH)?;
319 Ok(payload.to_vec())
320 }
321
322 fn expand_to_changes(
323 op: &AppDataUpdateOperation,
324 _prior: Option<&[u8]>,
325 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
326 match op {
327 AppDataUpdateOperation::Update(payload) => {
328 require_exact_len(Self::ID, payload.as_slice(), ED25519_KEY_LENGTH)?;
329 Ok(vec![ExpandedComponentChange {
330 op: ComponentOp::Update,
331 key: None,
332 value: Some(payload.as_slice().to_vec()),
333 }])
334 }
335 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
336 op: ComponentOp::Delete,
337 key: None,
338 value: None,
339 }]),
340 }
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::app_data::{
348 component_registry::ComponentRegistry,
349 validation::{ActorAuthority, ComponentChange},
350 };
351 use openmls::messages::proposals::AppDataUpdateOperation;
352
353 #[xmtp_common::test(unwrap_try = true)]
354 fn string_component_round_trip() {
355 let name = String::from("Test Group 🚀");
356 let bytes = GroupNameComponent::encode_value(&name).unwrap();
357 let decoded = GroupNameComponent::decode_value(&bytes).unwrap();
358 assert_eq!(decoded, name);
359 }
360
361 #[xmtp_common::test(unwrap_try = true)]
362 fn string_component_decode_rejects_non_utf8() {
363 let bytes = vec![0xff, 0xfe, 0xfd];
364 let err = GroupDescriptionComponent::decode_value(&bytes).unwrap_err();
365 match err {
366 ComponentTypedError::MalformedValue { component_id, .. } => {
367 assert_eq!(component_id, ComponentId::GROUP_DESCRIPTION);
368 }
369 other => panic!("expected MalformedValue, got {other:?}"),
370 }
371 }
372
373 #[xmtp_common::test(unwrap_try = true)]
378 fn string_component_apply_rejects_non_utf8() {
379 let err = AppDataComponent::apply_update_payload(&[0xff, 0xfe, 0xfd], None).unwrap_err();
380 match err {
381 ComponentTypedError::MalformedValue { component_id, .. } => {
382 assert_eq!(component_id, ComponentId::APP_DATA);
383 }
384 other => panic!("expected MalformedValue, got {other:?}"),
385 }
386 let ok = AppDataComponent::apply_update_payload("héllo".as_bytes(), None).unwrap();
388 assert_eq!(ok, "héllo".as_bytes());
389 }
390
391 #[xmtp_common::test(unwrap_try = true)]
395 fn string_component_expand_rejects_non_utf8() {
396 let op = AppDataUpdateOperation::Update(vec![0xff, 0xfe, 0xfd].into());
397 let err = GroupNameComponent::expand_to_changes(&op, None).unwrap_err();
398 match err {
399 ComponentTypedError::MalformedValue { component_id, .. } => {
400 assert_eq!(component_id, ComponentId::GROUP_NAME);
401 }
402 other => panic!("expected MalformedValue, got {other:?}"),
403 }
404 let changes =
406 GroupNameComponent::expand_to_changes(&AppDataUpdateOperation::Remove, None).unwrap();
407 assert_eq!(changes.len(), 1);
408 assert_eq!(changes[0].op, ComponentOp::Delete);
409 }
410
411 #[xmtp_common::test(unwrap_try = true)]
412 fn commit_log_signer_round_trip() {
413 let key = Secret::new(vec![0xAB; ED25519_KEY_LENGTH]);
414 let bytes = CommitLogSignerComponent::encode_value(&key).unwrap();
415 assert_eq!(bytes.len(), ED25519_KEY_LENGTH);
416 let decoded = CommitLogSignerComponent::decode_value(&bytes).unwrap();
417 assert_eq!(decoded.as_slice(), key.as_slice());
418 }
419
420 #[xmtp_common::test(unwrap_try = true)]
421 fn commit_log_signer_rejects_wrong_length() {
422 let too_short = vec![0u8; ED25519_KEY_LENGTH - 1];
423 let err = CommitLogSignerComponent::decode_value(&too_short).unwrap_err();
424 match err {
425 ComponentTypedError::MalformedValue { component_id, .. } => {
426 assert_eq!(component_id, ComponentId::COMMIT_LOG_SIGNER);
427 }
428 other => panic!("expected MalformedValue, got {other:?}"),
429 }
430 let wrong = Secret::new(vec![0u8; ED25519_KEY_LENGTH + 1]);
432 let err = CommitLogSignerComponent::encode_value(&wrong).unwrap_err();
433 assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
434 }
435
436 #[xmtp_common::test(unwrap_try = true)]
437 fn message_disappear_round_trip() {
438 let v: i64 = -1_234_567_890_123;
440 let bytes = MessageDisappearFromNsComponent::encode_value(&v).unwrap();
441 assert_eq!(bytes.len(), 8);
442 let decoded = MessageDisappearFromNsComponent::decode_value(&bytes).unwrap();
443 assert_eq!(decoded, v);
444 }
445
446 #[xmtp_common::test(unwrap_try = true)]
447 fn message_disappear_rejects_wrong_length() {
448 let ascii_decimal = b"3600000000000";
451 let err = MessageDisappearInNsComponent::decode_value(ascii_decimal).unwrap_err();
452 match err {
453 ComponentTypedError::MalformedValue { component_id, .. } => {
454 assert_eq!(component_id, ComponentId::MESSAGE_DISAPPEAR_IN_NS);
455 }
456 other => panic!("expected MalformedValue, got {other:?}"),
457 }
458 }
459
460 #[xmtp_common::test(unwrap_try = true)]
461 fn apply_update_passthrough() {
462 let payload = b"new value";
464 let new = GroupNameComponent::apply_update_payload(payload, Some(b"old")).unwrap();
465 assert_eq!(new, payload);
466
467 let valid = 42_i64.to_be_bytes();
470 let new = MessageDisappearInNsComponent::apply_update_payload(&valid, None).unwrap();
471 assert_eq!(new.as_slice(), valid.as_slice());
472
473 let err =
474 MessageDisappearInNsComponent::apply_update_payload(b"not 8 bytes", None).unwrap_err();
475 assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
476 }
477
478 #[xmtp_common::test(unwrap_try = true)]
479 fn expand_update_yields_one_change() {
480 let payload = b"hello".to_vec();
481 let op = AppDataUpdateOperation::Update(payload.clone().into());
482 let changes = GroupNameComponent::expand_to_changes(&op, None).unwrap();
483 assert_eq!(changes.len(), 1);
484 assert_eq!(changes[0].op, ComponentOp::Update);
485 assert_eq!(changes[0].value.as_deref(), Some(payload.as_slice()));
486 }
487
488 #[xmtp_common::test(unwrap_try = true)]
489 fn expand_remove_yields_delete_with_no_value() {
490 let op = AppDataUpdateOperation::Remove;
491 let changes = AppDataComponent::expand_to_changes(&op, None).unwrap();
492 assert_eq!(changes.len(), 1);
493 assert_eq!(changes[0].op, ComponentOp::Delete);
494 assert!(changes[0].value.is_none());
495 }
496
497 #[xmtp_common::test(unwrap_try = true)]
498 fn component_id_constants_match_static_id() {
499 assert_eq!(GroupNameComponent::ID, ComponentId::GROUP_NAME);
503 assert_eq!(
504 GroupDescriptionComponent::ID,
505 ComponentId::GROUP_DESCRIPTION
506 );
507 assert_eq!(GroupImageUrlComponent::ID, ComponentId::GROUP_IMAGE_URL);
508 assert_eq!(AppDataComponent::ID, ComponentId::APP_DATA);
509 assert_eq!(
510 MinSupportedProtocolVersionComponent::ID,
511 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
512 );
513 assert_eq!(
514 MessageDisappearFromNsComponent::ID,
515 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
516 );
517 assert_eq!(
518 MessageDisappearInNsComponent::ID,
519 ComponentId::MESSAGE_DISAPPEAR_IN_NS
520 );
521 assert_eq!(CommitLogSignerComponent::ID, ComponentId::COMMIT_LOG_SIGNER);
522 }
523
524 #[xmtp_common::test(unwrap_try = true)]
525 fn component_types_are_correct() {
526 assert_eq!(GroupNameComponent::COMPONENT_TYPE, ComponentType::String);
527 assert_eq!(
528 CommitLogSignerComponent::COMPONENT_TYPE,
529 ComponentType::Bytes
530 );
531 assert_eq!(
532 MessageDisappearFromNsComponent::COMPONENT_TYPE,
533 ComponentType::Bytes
534 );
535 assert_eq!(
536 MinSupportedProtocolVersionComponent::COMPONENT_TYPE,
537 ComponentType::String
538 );
539 }
540
541 macro_rules! bounded_component_test {
542 ($test_name:ident, $component:ty, $component_id:expr, $max_length:expr) => {
543 #[xmtp_common::test(unwrap_try = true)]
544 fn $test_name() {
545 let value = vec![b'x'; $max_length + 1];
546 let change = ComponentChange::builder()
547 .component_id($component_id)
548 .op(ComponentOp::Update)
549 .actor(ActorAuthority {
550 is_admin: true,
551 is_super_admin: true,
552 })
553 .new_value(&value)
554 .build();
555 let err = <$component>::validate_invariant(&change, &ComponentRegistry::new())
556 .unwrap_err();
557 assert!(matches!(err, ComponentInvariantError::Violation { .. }));
558 }
559 };
560 }
561
562 bounded_component_test!(
563 group_name_rejects_overlong_post_state,
564 GroupNameComponent,
565 ComponentId::GROUP_NAME,
566 MAX_GROUP_NAME_LENGTH
567 );
568 bounded_component_test!(
569 group_description_rejects_overlong_post_state,
570 GroupDescriptionComponent,
571 ComponentId::GROUP_DESCRIPTION,
572 MAX_GROUP_DESCRIPTION_LENGTH
573 );
574 bounded_component_test!(
575 group_image_url_rejects_overlong_post_state,
576 GroupImageUrlComponent,
577 ComponentId::GROUP_IMAGE_URL,
578 MAX_GROUP_IMAGE_URL_LENGTH
579 );
580 bounded_component_test!(
581 app_data_rejects_overlong_post_state,
582 AppDataComponent,
583 ComponentId::APP_DATA,
584 MAX_APP_DATA_LENGTH
585 );
586}