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, ComponentTypedError, ExpandedComponentChange},
34};
35
36pub(crate) fn apply_passthrough(payload: &[u8]) -> Result<Vec<u8>, ComponentTypedError> {
39 Ok(payload.to_vec())
40}
41
42pub(crate) fn expand_passthrough(
46 op: &AppDataUpdateOperation,
47) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
48 match op {
49 AppDataUpdateOperation::Update(payload) => Ok(vec![ExpandedComponentChange {
50 op: ComponentOp::Update,
51 value: Some(payload.as_slice().to_vec()),
52 }]),
53 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
54 op: ComponentOp::Delete,
55 value: None,
56 }]),
57 }
58}
59
60pub(crate) fn decode_utf8(
64 component_id: ComponentId,
65 bytes: &[u8],
66) -> Result<String, ComponentTypedError> {
67 std::str::from_utf8(bytes)
68 .map(str::to_owned)
69 .map_err(|err| ComponentTypedError::MalformedValue {
70 component_id,
71 reason: format!("not valid UTF-8: {err}"),
72 })
73}
74
75fn decode_be_i64(component_id: ComponentId, bytes: &[u8]) -> Result<i64, ComponentTypedError> {
78 let arr: [u8; 8] = bytes
79 .try_into()
80 .map_err(|_| ComponentTypedError::MalformedValue {
81 component_id,
82 reason: format!("expected 8 bytes (big-endian i64), got {}", bytes.len()),
83 })?;
84 Ok(i64::from_be_bytes(arr))
85}
86
87fn require_exact_len(
90 component_id: ComponentId,
91 bytes: &[u8],
92 expected: usize,
93) -> Result<(), ComponentTypedError> {
94 if bytes.len() != expected {
95 return Err(ComponentTypedError::MalformedValue {
96 component_id,
97 reason: format!("expected {} bytes, got {}", expected, bytes.len()),
98 });
99 }
100 Ok(())
101}
102
103macro_rules! passthrough_string_component {
114 ($struct_name:ident, $id:expr) => {
115 pub struct $struct_name;
116
117 impl Component for $struct_name {
118 const ID: ComponentId = $id;
119 const COMPONENT_TYPE: ComponentType = ComponentType::String;
120 type Value = String;
121 type Mutation = String;
122
123 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
124 decode_utf8(Self::ID, bytes)
125 }
126
127 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
128 Ok(value.as_bytes().to_vec())
129 }
130
131 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
132 Ok(mutation.as_bytes().to_vec())
133 }
134
135 fn apply_update_payload(
136 payload: &[u8],
137 _prior: Option<&[u8]>,
138 ) -> Result<Vec<u8>, ComponentTypedError> {
139 let _ = decode_utf8(Self::ID, payload)?;
150 apply_passthrough(payload)
151 }
152
153 fn expand_to_changes(
154 op: &AppDataUpdateOperation,
155 _prior: Option<&[u8]>,
156 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
157 if let AppDataUpdateOperation::Update(payload) = op {
159 let _ = decode_utf8(Self::ID, payload.as_slice())?;
160 }
161 expand_passthrough(op)
162 }
163 }
164 };
165}
166
167macro_rules! be_i64_component {
170 ($struct_name:ident, $id:expr) => {
171 pub struct $struct_name;
172
173 impl Component for $struct_name {
174 const ID: ComponentId = $id;
175 const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
176 type Value = i64;
177 type Mutation = i64;
178
179 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
180 decode_be_i64(Self::ID, bytes)
181 }
182
183 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
184 Ok(value.to_be_bytes().to_vec())
185 }
186
187 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
188 Ok(mutation.to_be_bytes().to_vec())
189 }
190
191 fn apply_update_payload(
192 payload: &[u8],
193 _prior: Option<&[u8]>,
194 ) -> Result<Vec<u8>, ComponentTypedError> {
195 let _ = decode_be_i64(Self::ID, payload)?;
197 Ok(payload.to_vec())
198 }
199
200 fn expand_to_changes(
201 op: &AppDataUpdateOperation,
202 _prior: Option<&[u8]>,
203 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
204 match op {
205 AppDataUpdateOperation::Update(payload) => {
206 let _ = decode_be_i64(Self::ID, payload.as_slice())?;
207 Ok(vec![ExpandedComponentChange {
208 op: ComponentOp::Update,
209 value: Some(payload.as_slice().to_vec()),
210 }])
211 }
212 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
213 op: ComponentOp::Delete,
214 value: None,
215 }]),
216 }
217 }
218 }
219 };
220}
221
222passthrough_string_component!(GroupNameComponent, ComponentId::GROUP_NAME);
223passthrough_string_component!(GroupDescriptionComponent, ComponentId::GROUP_DESCRIPTION);
224passthrough_string_component!(GroupImageUrlComponent, ComponentId::GROUP_IMAGE_URL);
225passthrough_string_component!(AppDataComponent, ComponentId::APP_DATA);
226passthrough_string_component!(
227 MinSupportedProtocolVersionComponent,
228 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
229);
230
231be_i64_component!(
232 MessageDisappearFromNsComponent,
233 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
234);
235be_i64_component!(
236 MessageDisappearInNsComponent,
237 ComponentId::MESSAGE_DISAPPEAR_IN_NS
238);
239
240pub struct CommitLogSignerComponent;
247
248impl Component for CommitLogSignerComponent {
249 const ID: ComponentId = ComponentId::COMMIT_LOG_SIGNER;
250 const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
251 type Value = Secret;
252 type Mutation = Secret;
253
254 fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
255 require_exact_len(Self::ID, bytes, ED25519_KEY_LENGTH)?;
256 Ok(Secret::new(bytes.to_vec()))
257 }
258
259 fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
260 require_exact_len(Self::ID, value.as_slice(), ED25519_KEY_LENGTH)?;
261 Ok(value.as_slice().to_vec())
262 }
263
264 fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
265 require_exact_len(Self::ID, mutation.as_slice(), ED25519_KEY_LENGTH)?;
266 Ok(mutation.as_slice().to_vec())
267 }
268
269 fn apply_update_payload(
270 payload: &[u8],
271 _prior: Option<&[u8]>,
272 ) -> Result<Vec<u8>, ComponentTypedError> {
273 require_exact_len(Self::ID, payload, ED25519_KEY_LENGTH)?;
274 Ok(payload.to_vec())
275 }
276
277 fn expand_to_changes(
278 op: &AppDataUpdateOperation,
279 _prior: Option<&[u8]>,
280 ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
281 match op {
282 AppDataUpdateOperation::Update(payload) => {
283 require_exact_len(Self::ID, payload.as_slice(), ED25519_KEY_LENGTH)?;
284 Ok(vec![ExpandedComponentChange {
285 op: ComponentOp::Update,
286 value: Some(payload.as_slice().to_vec()),
287 }])
288 }
289 AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
290 op: ComponentOp::Delete,
291 value: None,
292 }]),
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use openmls::messages::proposals::AppDataUpdateOperation;
301
302 #[xmtp_common::test(unwrap_try = true)]
303 fn string_component_round_trip() {
304 let name = String::from("Test Group 🚀");
305 let bytes = GroupNameComponent::encode_value(&name).unwrap();
306 let decoded = GroupNameComponent::decode_value(&bytes).unwrap();
307 assert_eq!(decoded, name);
308 }
309
310 #[xmtp_common::test(unwrap_try = true)]
311 fn string_component_decode_rejects_non_utf8() {
312 let bytes = vec![0xff, 0xfe, 0xfd];
313 let err = GroupDescriptionComponent::decode_value(&bytes).unwrap_err();
314 match err {
315 ComponentTypedError::MalformedValue { component_id, .. } => {
316 assert_eq!(component_id, ComponentId::GROUP_DESCRIPTION);
317 }
318 other => panic!("expected MalformedValue, got {other:?}"),
319 }
320 }
321
322 #[xmtp_common::test(unwrap_try = true)]
327 fn string_component_apply_rejects_non_utf8() {
328 let err = AppDataComponent::apply_update_payload(&[0xff, 0xfe, 0xfd], None).unwrap_err();
329 match err {
330 ComponentTypedError::MalformedValue { component_id, .. } => {
331 assert_eq!(component_id, ComponentId::APP_DATA);
332 }
333 other => panic!("expected MalformedValue, got {other:?}"),
334 }
335 let ok = AppDataComponent::apply_update_payload("héllo".as_bytes(), None).unwrap();
337 assert_eq!(ok, "héllo".as_bytes());
338 }
339
340 #[xmtp_common::test(unwrap_try = true)]
344 fn string_component_expand_rejects_non_utf8() {
345 let op = AppDataUpdateOperation::Update(vec![0xff, 0xfe, 0xfd].into());
346 let err = GroupNameComponent::expand_to_changes(&op, None).unwrap_err();
347 match err {
348 ComponentTypedError::MalformedValue { component_id, .. } => {
349 assert_eq!(component_id, ComponentId::GROUP_NAME);
350 }
351 other => panic!("expected MalformedValue, got {other:?}"),
352 }
353 let changes =
355 GroupNameComponent::expand_to_changes(&AppDataUpdateOperation::Remove, None).unwrap();
356 assert_eq!(changes.len(), 1);
357 assert_eq!(changes[0].op, ComponentOp::Delete);
358 }
359
360 #[xmtp_common::test(unwrap_try = true)]
361 fn commit_log_signer_round_trip() {
362 let key = Secret::new(vec![0xAB; ED25519_KEY_LENGTH]);
363 let bytes = CommitLogSignerComponent::encode_value(&key).unwrap();
364 assert_eq!(bytes.len(), ED25519_KEY_LENGTH);
365 let decoded = CommitLogSignerComponent::decode_value(&bytes).unwrap();
366 assert_eq!(decoded.as_slice(), key.as_slice());
367 }
368
369 #[xmtp_common::test(unwrap_try = true)]
370 fn commit_log_signer_rejects_wrong_length() {
371 let too_short = vec![0u8; ED25519_KEY_LENGTH - 1];
372 let err = CommitLogSignerComponent::decode_value(&too_short).unwrap_err();
373 match err {
374 ComponentTypedError::MalformedValue { component_id, .. } => {
375 assert_eq!(component_id, ComponentId::COMMIT_LOG_SIGNER);
376 }
377 other => panic!("expected MalformedValue, got {other:?}"),
378 }
379 let wrong = Secret::new(vec![0u8; ED25519_KEY_LENGTH + 1]);
381 let err = CommitLogSignerComponent::encode_value(&wrong).unwrap_err();
382 assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
383 }
384
385 #[xmtp_common::test(unwrap_try = true)]
386 fn message_disappear_round_trip() {
387 let v: i64 = -1_234_567_890_123;
389 let bytes = MessageDisappearFromNsComponent::encode_value(&v).unwrap();
390 assert_eq!(bytes.len(), 8);
391 let decoded = MessageDisappearFromNsComponent::decode_value(&bytes).unwrap();
392 assert_eq!(decoded, v);
393 }
394
395 #[xmtp_common::test(unwrap_try = true)]
396 fn message_disappear_rejects_wrong_length() {
397 let ascii_decimal = b"3600000000000";
400 let err = MessageDisappearInNsComponent::decode_value(ascii_decimal).unwrap_err();
401 match err {
402 ComponentTypedError::MalformedValue { component_id, .. } => {
403 assert_eq!(component_id, ComponentId::MESSAGE_DISAPPEAR_IN_NS);
404 }
405 other => panic!("expected MalformedValue, got {other:?}"),
406 }
407 }
408
409 #[xmtp_common::test(unwrap_try = true)]
410 fn apply_update_passthrough() {
411 let payload = b"new value";
413 let new = GroupNameComponent::apply_update_payload(payload, Some(b"old")).unwrap();
414 assert_eq!(new, payload);
415
416 let valid = 42_i64.to_be_bytes();
419 let new = MessageDisappearInNsComponent::apply_update_payload(&valid, None).unwrap();
420 assert_eq!(new.as_slice(), valid.as_slice());
421
422 let err =
423 MessageDisappearInNsComponent::apply_update_payload(b"not 8 bytes", None).unwrap_err();
424 assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
425 }
426
427 #[xmtp_common::test(unwrap_try = true)]
428 fn expand_update_yields_one_change() {
429 let payload = b"hello".to_vec();
430 let op = AppDataUpdateOperation::Update(payload.clone().into());
431 let changes = GroupNameComponent::expand_to_changes(&op, None).unwrap();
432 assert_eq!(changes.len(), 1);
433 assert_eq!(changes[0].op, ComponentOp::Update);
434 assert_eq!(changes[0].value.as_deref(), Some(payload.as_slice()));
435 }
436
437 #[xmtp_common::test(unwrap_try = true)]
438 fn expand_remove_yields_delete_with_no_value() {
439 let op = AppDataUpdateOperation::Remove;
440 let changes = AppDataComponent::expand_to_changes(&op, None).unwrap();
441 assert_eq!(changes.len(), 1);
442 assert_eq!(changes[0].op, ComponentOp::Delete);
443 assert!(changes[0].value.is_none());
444 }
445
446 #[xmtp_common::test(unwrap_try = true)]
447 fn component_id_constants_match_static_id() {
448 assert_eq!(GroupNameComponent::ID, ComponentId::GROUP_NAME);
452 assert_eq!(
453 GroupDescriptionComponent::ID,
454 ComponentId::GROUP_DESCRIPTION
455 );
456 assert_eq!(GroupImageUrlComponent::ID, ComponentId::GROUP_IMAGE_URL);
457 assert_eq!(AppDataComponent::ID, ComponentId::APP_DATA);
458 assert_eq!(
459 MinSupportedProtocolVersionComponent::ID,
460 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
461 );
462 assert_eq!(
463 MessageDisappearFromNsComponent::ID,
464 ComponentId::MESSAGE_DISAPPEAR_FROM_NS
465 );
466 assert_eq!(
467 MessageDisappearInNsComponent::ID,
468 ComponentId::MESSAGE_DISAPPEAR_IN_NS
469 );
470 assert_eq!(CommitLogSignerComponent::ID, ComponentId::COMMIT_LOG_SIGNER);
471 }
472
473 #[xmtp_common::test(unwrap_try = true)]
474 fn component_types_are_correct() {
475 assert_eq!(GroupNameComponent::COMPONENT_TYPE, ComponentType::String);
476 assert_eq!(
477 CommitLogSignerComponent::COMPONENT_TYPE,
478 ComponentType::Bytes
479 );
480 assert_eq!(
481 MessageDisappearFromNsComponent::COMPONENT_TYPE,
482 ComponentType::Bytes
483 );
484 assert_eq!(
485 MinSupportedProtocolVersionComponent::COMPONENT_TYPE,
486 ComponentType::String
487 );
488 }
489}