xmtp_mls_common/app_data/
component_id.rs1use std::fmt;
2use std::io::{Read, Write};
3
4use tls_codec::{Deserialize, Serialize, Size};
5
6const COMPONENT_RANGE_START: u16 = 0x8000;
16
17const XMTP_RANGE_START: u16 = 0x8000;
19const XMTP_RANGE_END: u16 = 0xBFFF;
20const XMTP_IMMUTABLE_START: u16 = 0xBE00;
22
23const APP_RANGE_START: u16 = *xmtp_configuration::APPLICATION_COMPONENT_IDS.start();
26const APP_RANGE_END: u16 = *xmtp_configuration::APPLICATION_COMPONENT_IDS.end();
27const APP_IMMUTABLE_START: u16 = 0xFD00;
29
30const COMPONENT_RESERVED_START: u16 = 0xFF00;
32
33const _RANGE_INVARIANTS: () = {
39 assert!(XMTP_RANGE_START == COMPONENT_RANGE_START);
41 assert!(XMTP_RANGE_START < XMTP_RANGE_END);
42 assert!(APP_RANGE_START == XMTP_RANGE_END + 1);
44 assert!(APP_RANGE_START < APP_RANGE_END);
45 assert!(COMPONENT_RESERVED_START == APP_RANGE_END + 1);
46 assert!(XMTP_IMMUTABLE_START > XMTP_RANGE_START);
48 assert!(XMTP_IMMUTABLE_START <= XMTP_RANGE_END);
49 assert!(APP_IMMUTABLE_START > APP_RANGE_START);
50 assert!(APP_IMMUTABLE_START <= APP_RANGE_END);
51};
52
53#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct ComponentId(u16);
63
64impl ComponentId {
65 pub const COMPONENT_REGISTRY: Self = Self(0x8000);
70 pub const SUPER_ADMIN_LIST: Self = Self(0x8001);
72 pub const ADMIN_LIST: Self = Self(0x8002);
77
78 pub const GROUP_MEMBERSHIP: Self = Self(0x8003);
81 pub const GROUP_NAME: Self = Self(0x8004);
82 pub const GROUP_DESCRIPTION: Self = Self(0x8005);
83 pub const GROUP_IMAGE_URL: Self = Self(0x8006);
84 pub const MESSAGE_DISAPPEAR_FROM_NS: Self = Self(0x8007);
85 pub const MESSAGE_DISAPPEAR_IN_NS: Self = Self(0x8008);
86 pub const APP_DATA: Self = Self(0x8009);
87 pub const MIN_SUPPORTED_PROTOCOL_VERSION: Self = Self(0x800A);
88 pub const COMMIT_LOG_SIGNER: Self = Self(0x800B);
89 pub const USER_DISPLAY_NAME: Self = Self(0x800C);
91 pub const GROUP_IMAGE: Self = Self(0x800D);
93
94 pub const CONVERSATION_TYPE: Self = Self(0xBFFF);
97 pub const CREATOR_INBOX_ID: Self = Self(0xBFFE);
98 pub const DM_MEMBERS: Self = Self(0xBFFD);
99 pub const ONESHOT_MESSAGE: Self = Self(0xBFFC);
100
101 pub const WELL_KNOWN_NAMES: [(Self, &'static str); 18] = [
103 (Self::COMPONENT_REGISTRY, "COMPONENT_REGISTRY"),
104 (Self::SUPER_ADMIN_LIST, "SUPER_ADMIN_LIST"),
105 (Self::ADMIN_LIST, "ADMIN_LIST"),
106 (Self::GROUP_MEMBERSHIP, "GROUP_MEMBERSHIP"),
107 (Self::GROUP_NAME, "GROUP_NAME"),
108 (Self::GROUP_DESCRIPTION, "GROUP_DESCRIPTION"),
109 (Self::GROUP_IMAGE_URL, "GROUP_IMAGE_URL"),
110 (Self::MESSAGE_DISAPPEAR_FROM_NS, "MESSAGE_DISAPPEAR_FROM_NS"),
111 (Self::MESSAGE_DISAPPEAR_IN_NS, "MESSAGE_DISAPPEAR_IN_NS"),
112 (Self::APP_DATA, "APP_DATA"),
113 (
114 Self::MIN_SUPPORTED_PROTOCOL_VERSION,
115 "MIN_SUPPORTED_PROTOCOL_VERSION",
116 ),
117 (Self::COMMIT_LOG_SIGNER, "COMMIT_LOG_SIGNER"),
118 (Self::USER_DISPLAY_NAME, "USER_DISPLAY_NAME"),
119 (Self::GROUP_IMAGE, "GROUP_IMAGE"),
120 (Self::ONESHOT_MESSAGE, "ONESHOT_MESSAGE"),
121 (Self::DM_MEMBERS, "DM_MEMBERS"),
122 (Self::CREATOR_INBOX_ID, "CREATOR_INBOX_ID"),
123 (Self::CONVERSATION_TYPE, "CONVERSATION_TYPE"),
124 ];
125
126 pub fn from_well_known_name(name: &str) -> Option<Self> {
128 Self::WELL_KNOWN_NAMES
129 .iter()
130 .find_map(|&(id, known)| (known == name).then_some(id))
131 }
132
133 pub const fn new(id: u16) -> Self {
136 Self(id)
137 }
138
139 pub const fn as_u16(self) -> u16 {
140 self.0
141 }
142
143 pub fn event_name(self) -> String {
145 if let Some(field) = super::component_source::component_id_to_metadata_field(self) {
146 return field.as_str().into();
147 }
148 match self {
149 Self::COMPONENT_REGISTRY => "COMPONENT_REGISTRY".into(),
150 Self::SUPER_ADMIN_LIST => "SUPER_ADMIN_LIST".into(),
151 Self::ADMIN_LIST => "ADMIN_LIST".into(),
152 Self::GROUP_MEMBERSHIP => "GROUP_MEMBERSHIP".into(),
153 Self::USER_DISPLAY_NAME => "USER_DISPLAY_NAME".into(),
154 Self::GROUP_IMAGE => "GROUP_IMAGE".into(),
155 Self::CONVERSATION_TYPE => "CONVERSATION_TYPE".into(),
156 Self::CREATOR_INBOX_ID => "CREATOR_INBOX_ID".into(),
157 Self::DM_MEMBERS => "DM_MEMBERS".into(),
158 Self::ONESHOT_MESSAGE => "ONESHOT_MESSAGE".into(),
159 _ => format!("component:{:04x}", self.0),
160 }
161 }
162
163 pub const fn is_in_component_space(self) -> bool {
169 self.0 >= COMPONENT_RANGE_START
170 }
171
172 pub const fn is_xmtp_range(self) -> bool {
174 self.0 >= XMTP_RANGE_START && self.0 <= XMTP_RANGE_END
175 }
176
177 pub const fn is_app_range(self) -> bool {
179 self.0 >= APP_RANGE_START && self.0 <= APP_RANGE_END
180 }
181
182 pub const fn is_reserved(self) -> bool {
184 self.0 >= COMPONENT_RESERVED_START
185 }
186
187 pub const fn is_immutable(self) -> bool {
190 (self.0 >= XMTP_IMMUTABLE_START && self.0 <= XMTP_RANGE_END)
191 || (self.0 >= APP_IMMUTABLE_START && self.0 <= APP_RANGE_END)
192 }
193
194 pub const fn is_hardcoded(self) -> bool {
197 self.0 == Self::COMPONENT_REGISTRY.0 || self.0 == Self::SUPER_ADMIN_LIST.0
198 }
199
200 pub const fn is_constrained(self) -> bool {
205 self.0 == Self::ADMIN_LIST.0
206 }
207}
208
209impl fmt::Debug for ComponentId {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 write!(f, "ComponentId(0x{:04X})", self.0)
212 }
213}
214
215impl fmt::Display for ComponentId {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 write!(f, "0x{:04X}", self.0)
218 }
219}
220
221impl From<u16> for ComponentId {
222 fn from(id: u16) -> Self {
223 Self(id)
224 }
225}
226
227impl From<ComponentId> for u16 {
228 fn from(id: ComponentId) -> Self {
229 id.0
230 }
231}
232
233fn vlen_encoding_bytes(value: usize) -> usize {
250 let mut buf = [0u8; 8];
251 let mut slice: &mut [u8] = &mut buf;
252 tls_codec::vlen::write_length(&mut slice, value)
253 .expect("8-byte buffer fits any QUIC vlen encoding")
254}
255
256impl Size for ComponentId {
257 fn tls_serialized_len(&self) -> usize {
258 vlen_encoding_bytes(self.0 as usize)
259 }
260}
261
262impl Serialize for ComponentId {
264 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, tls_codec::Error> {
265 tls_codec::vlen::write_length(writer, self.0 as usize)
266 }
267}
268
269impl Deserialize for ComponentId {
271 fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, tls_codec::Error>
272 where
273 Self: Sized,
274 {
275 let (value, _len_len) = tls_codec::vlen::read_length(bytes)?;
276 if value > u16::MAX as usize {
277 return Err(tls_codec::Error::DecodingError(format!(
278 "ComponentId value {value} exceeds u16::MAX; this version of \
279 the library cannot decode IDs larger than 0xFFFF"
280 )));
281 }
282 Ok(Self(value as u16))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use tls_codec::{Deserialize, Serialize};
290
291 #[xmtp_common::test]
292 fn event_names_cover_known_and_application_components() {
293 assert_eq!(ComponentId::GROUP_NAME.event_name(), "group_name");
294 assert_eq!(ComponentId::ADMIN_LIST.event_name(), "ADMIN_LIST");
295 assert_eq!(
296 ComponentId::USER_DISPLAY_NAME.event_name(),
297 "USER_DISPLAY_NAME"
298 );
299 assert_eq!(ComponentId::GROUP_IMAGE.event_name(), "GROUP_IMAGE");
300 assert_eq!(ComponentId::new(0xc123).event_name(), "component:c123");
301 }
302
303 #[xmtp_common::test]
307 fn well_known_names_match_exactly() {
308 assert_eq!(
309 ComponentId::from_well_known_name("GROUP_NAME"),
310 Some(ComponentId::GROUP_NAME)
311 );
312 assert_eq!(
313 ComponentId::from_well_known_name("CONVERSATION_TYPE"),
314 Some(ComponentId::CONVERSATION_TYPE)
315 );
316 for other in ["group_name", "GROUP_NAME ", "USER_PRONOUNS", ""] {
317 assert_eq!(ComponentId::from_well_known_name(other), None);
318 }
319 for pair in ComponentId::WELL_KNOWN_NAMES.windows(2) {
320 assert!(pair[0].0 < pair[1].0, "{pair:?} is out of ID order");
321 }
322 for id in (XMTP_RANGE_START..=XMTP_RANGE_END).map(ComponentId::new) {
324 let listed = ComponentId::WELL_KNOWN_NAMES
325 .iter()
326 .any(|(known, _)| *known == id);
327 assert_eq!(listed, !id.event_name().starts_with("component:"), "{id:?}");
328 }
329 }
330
331 #[xmtp_common::test]
332 fn test_well_known_ids_are_in_expected_ranges() {
333 assert!(ComponentId::COMPONENT_REGISTRY.is_hardcoded());
335 assert!(ComponentId::SUPER_ADMIN_LIST.is_hardcoded());
336 assert!(!ComponentId::ADMIN_LIST.is_hardcoded());
337
338 assert!(ComponentId::ADMIN_LIST.is_constrained());
340 assert!(!ComponentId::GROUP_NAME.is_constrained());
341
342 assert!(ComponentId::GROUP_MEMBERSHIP.is_xmtp_range());
344 assert!(ComponentId::GROUP_NAME.is_xmtp_range());
345 assert!(!ComponentId::GROUP_NAME.is_immutable());
346 assert!(!ComponentId::GROUP_NAME.is_hardcoded());
347 assert!(ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.is_xmtp_range());
348 assert!(!ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.is_immutable());
349 assert!(ComponentId::COMMIT_LOG_SIGNER.is_xmtp_range());
350 assert!(!ComponentId::COMMIT_LOG_SIGNER.is_immutable());
351 assert!(ComponentId::USER_DISPLAY_NAME.is_xmtp_range());
352 assert!(!ComponentId::USER_DISPLAY_NAME.is_immutable());
353 assert!(ComponentId::GROUP_IMAGE.is_xmtp_range());
354 assert!(!ComponentId::GROUP_IMAGE.is_immutable());
355
356 assert!(ComponentId::CONVERSATION_TYPE.is_immutable());
358 assert!(ComponentId::CREATOR_INBOX_ID.is_immutable());
359 assert!(ComponentId::CONVERSATION_TYPE.is_xmtp_range());
360 assert!(ComponentId::DM_MEMBERS.is_immutable());
361 assert!(ComponentId::DM_MEMBERS.is_xmtp_range());
362 assert!(ComponentId::ONESHOT_MESSAGE.is_immutable());
363 assert!(ComponentId::ONESHOT_MESSAGE.is_xmtp_range());
364 }
365
366 #[xmtp_common::test]
367 fn test_range_boundaries() {
368 assert!(ComponentId::new(0x8003).is_xmtp_range());
370 assert!(!ComponentId::new(0x8003).is_immutable());
371
372 assert!(ComponentId::new(0x800A).is_xmtp_range());
374 assert!(!ComponentId::new(0x800A).is_immutable());
375 assert!(ComponentId::new(0x800B).is_xmtp_range());
376 assert!(!ComponentId::new(0x800B).is_immutable());
377
378 assert!(!ComponentId::new(0xBDFF).is_immutable());
380 assert!(ComponentId::new(0xBE00).is_immutable());
381 assert!(ComponentId::new(0xBFFF).is_immutable());
382
383 assert!(ComponentId::new(0xBFFD).is_immutable());
386 assert!(ComponentId::new(0xBFFC).is_immutable());
387
388 assert!(ComponentId::new(0xC000).is_app_range());
390 assert!(!ComponentId::new(0xC000).is_immutable());
391
392 assert!(!ComponentId::new(0xFCFF).is_immutable());
394 assert!(ComponentId::new(0xFD00).is_immutable());
395 assert!(ComponentId::new(0xFEFF).is_immutable());
396
397 assert!(ComponentId::new(0xFF00).is_reserved());
399 assert!(ComponentId::new(0xFFFF).is_reserved());
400 assert!(!ComponentId::new(0xFEFF).is_reserved());
401 }
402
403 #[xmtp_common::test]
404 fn test_is_in_component_space() {
405 assert!(!ComponentId::new(0x0000).is_in_component_space());
406 assert!(!ComponentId::new(0x7FFF).is_in_component_space());
407 assert!(ComponentId::new(0x8000).is_in_component_space());
408 assert!(ComponentId::new(0xFFFF).is_in_component_space());
409 }
410
411 #[xmtp_common::test]
412 fn test_ranges_are_mutually_exclusive() {
413 for id in [0x8000u16, 0x8500, 0xBFFF] {
415 let c = ComponentId::new(id);
416 assert!(c.is_xmtp_range());
417 assert!(!c.is_app_range());
418 assert!(!c.is_reserved());
419 }
420 for id in [0xC000u16, 0xD000, 0xFEFF] {
421 let c = ComponentId::new(id);
422 assert!(!c.is_xmtp_range());
423 assert!(c.is_app_range());
424 assert!(!c.is_reserved());
425 }
426 for id in [0xFF00u16, 0xFFFF] {
427 let c = ComponentId::new(id);
428 assert!(!c.is_xmtp_range());
429 assert!(!c.is_app_range());
430 assert!(c.is_reserved());
431 }
432 }
433
434 #[xmtp_common::test]
435 fn test_tls_codec_round_trip() {
436 let ids = [
440 ComponentId::new(0x0000),
441 ComponentId::new(0x003F), ComponentId::new(0x0040), ComponentId::new(0x3FFF), ComponentId::new(0x4000), ComponentId::COMPONENT_REGISTRY,
446 ComponentId::GROUP_NAME,
447 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
448 ComponentId::COMMIT_LOG_SIGNER,
449 ComponentId::CONVERSATION_TYPE,
450 ComponentId::DM_MEMBERS,
451 ComponentId::ONESHOT_MESSAGE,
452 ComponentId::new(0xFFFF),
453 ];
454 for id in ids {
455 let bytes = id.tls_serialize_detached().unwrap();
456 let deserialized = ComponentId::tls_deserialize_exact(&bytes).unwrap();
457 assert_eq!(id, deserialized, "round trip failed for {id:?}");
458 assert_eq!(
459 bytes.len(),
460 id.tls_serialized_len(),
461 "tls_serialized_len mismatch for {id:?}"
462 );
463 }
464 }
465
466 #[xmtp_common::test]
467 fn test_vlen_encoding_sizes() {
469 assert_eq!(ComponentId::new(0).tls_serialized_len(), 1);
471 assert_eq!(ComponentId::new(0x3F).tls_serialized_len(), 1);
472 assert_eq!(ComponentId::new(0x40).tls_serialized_len(), 2);
474 assert_eq!(ComponentId::new(0x3FFF).tls_serialized_len(), 2);
475 assert_eq!(ComponentId::new(0x4000).tls_serialized_len(), 4);
477 assert_eq!(ComponentId::new(0x8000).tls_serialized_len(), 4);
478 assert_eq!(ComponentId::new(0xFFFF).tls_serialized_len(), 4);
479 }
480
481 #[xmtp_common::test]
482 fn test_decode_rejects_value_above_u16_max() {
484 let bytes = [0x80, 0x01, 0x00, 0x00];
487 let result = ComponentId::tls_deserialize_exact(bytes);
488 assert!(matches!(result, Err(tls_codec::Error::DecodingError(_))));
489 }
490
491 #[xmtp_common::test]
492 fn test_ordering() {
493 let a = ComponentId::new(0x8000);
494 let b = ComponentId::new(0x8001);
495 let c = ComponentId::new(0xBFFF);
496 assert!(a < b);
497 assert!(b < c);
498 }
499
500 #[xmtp_common::test]
501 fn test_debug_display() {
502 let id = ComponentId::new(0x8004);
503 assert_eq!(format!("{id:?}"), "ComponentId(0x8004)");
504 assert_eq!(format!("{id}"), "0x8004");
505 }
506}