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 = 0xC000;
25const APP_RANGE_END: u16 = 0xFEFF;
26const APP_IMMUTABLE_START: u16 = 0xFD00;
28
29const COMPONENT_RESERVED_START: u16 = 0xFF00;
31
32const _RANGE_INVARIANTS: () = {
38 assert!(XMTP_RANGE_START == COMPONENT_RANGE_START);
40 assert!(XMTP_RANGE_START < XMTP_RANGE_END);
41 assert!(APP_RANGE_START == XMTP_RANGE_END + 1);
43 assert!(APP_RANGE_START < APP_RANGE_END);
44 assert!(COMPONENT_RESERVED_START == APP_RANGE_END + 1);
45 assert!(XMTP_IMMUTABLE_START > XMTP_RANGE_START);
47 assert!(XMTP_IMMUTABLE_START <= XMTP_RANGE_END);
48 assert!(APP_IMMUTABLE_START > APP_RANGE_START);
49 assert!(APP_IMMUTABLE_START <= APP_RANGE_END);
50};
51
52#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct ComponentId(u16);
62
63impl ComponentId {
64 pub const COMPONENT_REGISTRY: Self = Self(0x8000);
69 pub const SUPER_ADMIN_LIST: Self = Self(0x8001);
71 pub const ADMIN_LIST: Self = Self(0x8002);
73
74 pub const GROUP_MEMBERSHIP: Self = Self(0x8003);
77 pub const GROUP_NAME: Self = Self(0x8004);
78 pub const GROUP_DESCRIPTION: Self = Self(0x8005);
79 pub const GROUP_IMAGE_URL: Self = Self(0x8006);
80 pub const MESSAGE_DISAPPEAR_FROM_NS: Self = Self(0x8007);
81 pub const MESSAGE_DISAPPEAR_IN_NS: Self = Self(0x8008);
82 pub const APP_DATA: Self = Self(0x8009);
83 pub const MIN_SUPPORTED_PROTOCOL_VERSION: Self = Self(0x800A);
84 pub const COMMIT_LOG_SIGNER: Self = Self(0x800B);
85
86 pub const CONVERSATION_TYPE: Self = Self(0xBFFF);
89 pub const CREATOR_INBOX_ID: Self = Self(0xBFFE);
90 pub const DM_MEMBERS: Self = Self(0xBFFD);
91 pub const ONESHOT_MESSAGE: Self = Self(0xBFFC);
92
93 pub const fn new(id: u16) -> Self {
96 Self(id)
97 }
98
99 pub const fn as_u16(self) -> u16 {
100 self.0
101 }
102
103 pub const fn is_in_component_space(self) -> bool {
109 self.0 >= COMPONENT_RANGE_START
110 }
111
112 pub const fn is_xmtp_range(self) -> bool {
114 self.0 >= XMTP_RANGE_START && self.0 <= XMTP_RANGE_END
115 }
116
117 pub const fn is_app_range(self) -> bool {
119 self.0 >= APP_RANGE_START && self.0 <= APP_RANGE_END
120 }
121
122 pub const fn is_reserved(self) -> bool {
124 self.0 >= COMPONENT_RESERVED_START
125 }
126
127 pub const fn is_immutable(self) -> bool {
130 (self.0 >= XMTP_IMMUTABLE_START && self.0 <= XMTP_RANGE_END)
131 || (self.0 >= APP_IMMUTABLE_START && self.0 <= APP_RANGE_END)
132 }
133
134 pub const fn is_hardcoded(self) -> bool {
137 self.0 == Self::COMPONENT_REGISTRY.0 || self.0 == Self::SUPER_ADMIN_LIST.0
138 }
139
140 pub const fn is_constrained(self) -> bool {
145 self.0 == Self::ADMIN_LIST.0
146 }
147}
148
149impl fmt::Debug for ComponentId {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 write!(f, "ComponentId(0x{:04X})", self.0)
152 }
153}
154
155impl fmt::Display for ComponentId {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "0x{:04X}", self.0)
158 }
159}
160
161impl From<u16> for ComponentId {
162 fn from(id: u16) -> Self {
163 Self(id)
164 }
165}
166
167impl From<ComponentId> for u16 {
168 fn from(id: ComponentId) -> Self {
169 id.0
170 }
171}
172
173fn vlen_encoding_bytes(value: usize) -> usize {
190 let mut buf = [0u8; 8];
191 let mut slice: &mut [u8] = &mut buf;
192 tls_codec::vlen::write_length(&mut slice, value)
193 .expect("8-byte buffer fits any QUIC vlen encoding")
194}
195
196impl Size for ComponentId {
197 fn tls_serialized_len(&self) -> usize {
198 vlen_encoding_bytes(self.0 as usize)
199 }
200}
201
202impl Serialize for ComponentId {
203 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, tls_codec::Error> {
204 tls_codec::vlen::write_length(writer, self.0 as usize)
205 }
206}
207
208impl Deserialize for ComponentId {
209 fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, tls_codec::Error>
210 where
211 Self: Sized,
212 {
213 let (value, _len_len) = tls_codec::vlen::read_length(bytes)?;
214 if value > u16::MAX as usize {
215 return Err(tls_codec::Error::DecodingError(format!(
216 "ComponentId value {value} exceeds u16::MAX; this version of \
217 the library cannot decode IDs larger than 0xFFFF"
218 )));
219 }
220 Ok(Self(value as u16))
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use tls_codec::{Deserialize, Serialize};
228
229 #[xmtp_common::test]
230 fn test_well_known_ids_are_in_expected_ranges() {
231 assert!(ComponentId::COMPONENT_REGISTRY.is_hardcoded());
233 assert!(ComponentId::SUPER_ADMIN_LIST.is_hardcoded());
234 assert!(!ComponentId::ADMIN_LIST.is_hardcoded());
235
236 assert!(ComponentId::ADMIN_LIST.is_constrained());
238 assert!(!ComponentId::GROUP_NAME.is_constrained());
239
240 assert!(ComponentId::GROUP_MEMBERSHIP.is_xmtp_range());
242 assert!(ComponentId::GROUP_NAME.is_xmtp_range());
243 assert!(!ComponentId::GROUP_NAME.is_immutable());
244 assert!(!ComponentId::GROUP_NAME.is_hardcoded());
245 assert!(ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.is_xmtp_range());
246 assert!(!ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.is_immutable());
247 assert!(ComponentId::COMMIT_LOG_SIGNER.is_xmtp_range());
248 assert!(!ComponentId::COMMIT_LOG_SIGNER.is_immutable());
249
250 assert!(ComponentId::CONVERSATION_TYPE.is_immutable());
252 assert!(ComponentId::CREATOR_INBOX_ID.is_immutable());
253 assert!(ComponentId::CONVERSATION_TYPE.is_xmtp_range());
254 assert!(ComponentId::DM_MEMBERS.is_immutable());
255 assert!(ComponentId::DM_MEMBERS.is_xmtp_range());
256 assert!(ComponentId::ONESHOT_MESSAGE.is_immutable());
257 assert!(ComponentId::ONESHOT_MESSAGE.is_xmtp_range());
258 }
259
260 #[xmtp_common::test]
261 fn test_range_boundaries() {
262 assert!(ComponentId::new(0x8003).is_xmtp_range());
264 assert!(!ComponentId::new(0x8003).is_immutable());
265
266 assert!(ComponentId::new(0x800A).is_xmtp_range());
268 assert!(!ComponentId::new(0x800A).is_immutable());
269 assert!(ComponentId::new(0x800B).is_xmtp_range());
270 assert!(!ComponentId::new(0x800B).is_immutable());
271
272 assert!(!ComponentId::new(0xBDFF).is_immutable());
274 assert!(ComponentId::new(0xBE00).is_immutable());
275 assert!(ComponentId::new(0xBFFF).is_immutable());
276
277 assert!(ComponentId::new(0xBFFD).is_immutable());
280 assert!(ComponentId::new(0xBFFC).is_immutable());
281
282 assert!(ComponentId::new(0xC000).is_app_range());
284 assert!(!ComponentId::new(0xC000).is_immutable());
285
286 assert!(!ComponentId::new(0xFCFF).is_immutable());
288 assert!(ComponentId::new(0xFD00).is_immutable());
289 assert!(ComponentId::new(0xFEFF).is_immutable());
290
291 assert!(ComponentId::new(0xFF00).is_reserved());
293 assert!(ComponentId::new(0xFFFF).is_reserved());
294 assert!(!ComponentId::new(0xFEFF).is_reserved());
295 }
296
297 #[xmtp_common::test]
298 fn test_is_in_component_space() {
299 assert!(!ComponentId::new(0x0000).is_in_component_space());
300 assert!(!ComponentId::new(0x7FFF).is_in_component_space());
301 assert!(ComponentId::new(0x8000).is_in_component_space());
302 assert!(ComponentId::new(0xFFFF).is_in_component_space());
303 }
304
305 #[xmtp_common::test]
306 fn test_ranges_are_mutually_exclusive() {
307 for id in [0x8000u16, 0x8500, 0xBFFF] {
309 let c = ComponentId::new(id);
310 assert!(c.is_xmtp_range());
311 assert!(!c.is_app_range());
312 assert!(!c.is_reserved());
313 }
314 for id in [0xC000u16, 0xD000, 0xFEFF] {
315 let c = ComponentId::new(id);
316 assert!(!c.is_xmtp_range());
317 assert!(c.is_app_range());
318 assert!(!c.is_reserved());
319 }
320 for id in [0xFF00u16, 0xFFFF] {
321 let c = ComponentId::new(id);
322 assert!(!c.is_xmtp_range());
323 assert!(!c.is_app_range());
324 assert!(c.is_reserved());
325 }
326 }
327
328 #[xmtp_common::test]
329 fn test_tls_codec_round_trip() {
330 let ids = [
334 ComponentId::new(0x0000),
335 ComponentId::new(0x003F), ComponentId::new(0x0040), ComponentId::new(0x3FFF), ComponentId::new(0x4000), ComponentId::COMPONENT_REGISTRY,
340 ComponentId::GROUP_NAME,
341 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
342 ComponentId::COMMIT_LOG_SIGNER,
343 ComponentId::CONVERSATION_TYPE,
344 ComponentId::DM_MEMBERS,
345 ComponentId::ONESHOT_MESSAGE,
346 ComponentId::new(0xFFFF),
347 ];
348 for id in ids {
349 let bytes = id.tls_serialize_detached().unwrap();
350 let deserialized = ComponentId::tls_deserialize_exact(&bytes).unwrap();
351 assert_eq!(id, deserialized, "round trip failed for {id:?}");
352 assert_eq!(
353 bytes.len(),
354 id.tls_serialized_len(),
355 "tls_serialized_len mismatch for {id:?}"
356 );
357 }
358 }
359
360 #[xmtp_common::test]
361 fn test_vlen_encoding_sizes() {
362 assert_eq!(ComponentId::new(0).tls_serialized_len(), 1);
364 assert_eq!(ComponentId::new(0x3F).tls_serialized_len(), 1);
365 assert_eq!(ComponentId::new(0x40).tls_serialized_len(), 2);
367 assert_eq!(ComponentId::new(0x3FFF).tls_serialized_len(), 2);
368 assert_eq!(ComponentId::new(0x4000).tls_serialized_len(), 4);
370 assert_eq!(ComponentId::new(0x8000).tls_serialized_len(), 4);
371 assert_eq!(ComponentId::new(0xFFFF).tls_serialized_len(), 4);
372 }
373
374 #[xmtp_common::test]
375 fn test_decode_rejects_value_above_u16_max() {
376 let bytes = [0x80, 0x01, 0x00, 0x00];
379 let result = ComponentId::tls_deserialize_exact(bytes);
380 assert!(matches!(result, Err(tls_codec::Error::DecodingError(_))));
381 }
382
383 #[xmtp_common::test]
384 fn test_ordering() {
385 let a = ComponentId::new(0x8000);
386 let b = ComponentId::new(0x8001);
387 let c = ComponentId::new(0xBFFF);
388 assert!(a < b);
389 assert!(b < c);
390 }
391
392 #[xmtp_common::test]
393 fn test_debug_display() {
394 let id = ComponentId::new(0x8004);
395 assert_eq!(format!("{id:?}"), "ComponentId(0x8004)");
396 assert_eq!(format!("{id}"), "0x8004");
397 }
398}