Skip to main content

xmtp_mls_common/app_data/
component_id.rs

1use std::fmt;
2use std::io::{Read, Write};
3
4use tls_codec::{Deserialize, Serialize, Size};
5
6// ============================================================================
7// Component ID Ranges
8// ============================================================================
9//
10// ComponentIds occupy the top half of the u16 space (0x8000-0xFFFF).
11// The space is split between XMTP protocol and application use, with
12// immutable ranges at the end of each block (counting down).
13
14/// Start of the component ID space (top half of u16).
15const COMPONENT_RANGE_START: u16 = 0x8000;
16
17// --- XMTP Protocol Range: 0x8000-0xBFFF ---
18const XMTP_RANGE_START: u16 = 0x8000;
19const XMTP_RANGE_END: u16 = 0xBFFF;
20/// Immutable XMTP components: 0xBE00-0xBFFF (512 IDs, counting down).
21const XMTP_IMMUTABLE_START: u16 = 0xBE00;
22
23// --- Application Range: 0xC000-0xFEFF ---
24// The backend configuration validates operator IDs against the same range.
25const APP_RANGE_START: u16 = *xmtp_configuration::APPLICATION_COMPONENT_IDS.start();
26const APP_RANGE_END: u16 = *xmtp_configuration::APPLICATION_COMPONENT_IDS.end();
27/// Immutable application components: 0xFD00-0xFEFF (512 IDs, counting down).
28const APP_IMMUTABLE_START: u16 = 0xFD00;
29
30// --- Reserved: 0xFF00-0xFFFF ---
31const COMPONENT_RESERVED_START: u16 = 0xFF00;
32
33// Compile-time invariant checks: if any of the range constants are ever
34// changed in a way that breaks these assumptions, this will fail to build.
35// The validation logic in `ComponentRegistry` relies on these — the ranges
36// must be contiguous, non-overlapping, and immutable subranges must sit at
37// the end of their parent block.
38const _RANGE_INVARIANTS: () = {
39    // The component space starts at exactly the XMTP range.
40    assert!(XMTP_RANGE_START == COMPONENT_RANGE_START);
41    assert!(XMTP_RANGE_START < XMTP_RANGE_END);
42    // XMTP, App, and Reserved are contiguous and non-overlapping.
43    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    // Immutable subranges sit strictly inside their parent block, at the end.
47    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/// A component identifier in the XMTP app data system.
54///
55/// ComponentIds occupy the top half of the u16 space (`0x8000-0xFFFF`), split
56/// between XMTP protocol use (`0x8000-0xBFFF`) and application-defined
57/// components (`0xC000-0xFEFF`), with `0xFF00-0xFFFF` reserved.
58///
59/// Immutable ranges sit at the end of each block (last 512 IDs, counting down).
60/// Components in these ranges can be written once but never updated or deleted.
61#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct ComponentId(u16);
63
64impl ComponentId {
65    // === Hardcoded Component IDs ===
66    // Permissions for these are enforced in code, not in the permissions map.
67
68    /// The component registry. Super admin only.
69    pub const COMPONENT_REGISTRY: Self = Self(0x8000);
70    /// The super admin list. Super admin only.
71    pub const SUPER_ADMIN_LIST: Self = Self(0x8001);
72    // === Constrained Component IDs ===
73    // Stored policies are limited to Deny, AllowIfAdmin, or AllowIfSuperAdmin.
74
75    /// The admin list. Configurable: super admin only or admin/super admin.
76    pub const ADMIN_LIST: Self = Self(0x8002);
77
78    // === Well-Known Mutable XMTP Component IDs (counting up from 0x8003) ===
79
80    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    /// Per-inbox display names: `TlsMap<InboxId, UTF-8 bytes>`.
90    pub const USER_DISPLAY_NAME: Self = Self(0x800C);
91    /// Opaque group image bytes, such as a remote attachment descriptor.
92    pub const GROUP_IMAGE: Self = Self(0x800D);
93
94    // === Well-Known Immutable XMTP Component IDs (counting down from 0xBFFF) ===
95
96    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    /// Every well-known component and its META section 2 name, in ID order.
102    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    /// The well-known component with exactly this name.
127    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    // === Constructor and Accessors ===
134
135    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    /// Name this component in a metadata change event.
144    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    // === Range Helpers ===
164
165    /// Returns true if the ID is in the component ID space (top half of u16).
166    /// Note: this includes the reserved range (`0xFF00-0xFFFF`); use
167    /// [`is_reserved`](Self::is_reserved) to distinguish.
168    pub const fn is_in_component_space(self) -> bool {
169        self.0 >= COMPONENT_RANGE_START
170    }
171
172    /// Returns true if the ID is in the XMTP protocol range (`0x8000-0xBFFF`).
173    pub const fn is_xmtp_range(self) -> bool {
174        self.0 >= XMTP_RANGE_START && self.0 <= XMTP_RANGE_END
175    }
176
177    /// Returns true if the ID is in the application range (`0xC000-0xFEFF`).
178    pub const fn is_app_range(self) -> bool {
179        self.0 >= APP_RANGE_START && self.0 <= APP_RANGE_END
180    }
181
182    /// Returns true if the ID is in the reserved range (`0xFF00-0xFFFF`).
183    pub const fn is_reserved(self) -> bool {
184        self.0 >= COMPONENT_RESERVED_START
185    }
186
187    /// Returns true if the ID is in an immutable range.
188    /// Immutable components can be inserted once but never updated or deleted.
189    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    /// Returns true if this is one of the hardcoded components whose
195    /// permissions are enforced in code rather than the component registry.
196    pub const fn is_hardcoded(self) -> bool {
197        self.0 == Self::COMPONENT_REGISTRY.0 || self.0 == Self::SUPER_ADMIN_LIST.0
198    }
199
200    /// Returns true if this component has constrained permission values.
201    /// Constrained components can only have their permissions set to the
202    /// proto base policies `AllowIfAdmin` (admin or super admin) or
203    /// `AllowIfSuperAdmin` (super admin only).
204    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
233// === TLS Codec ===
234//
235// ComponentIds are encoded using QUIC variable-length integer encoding
236// (RFC 9000 §16) rather than a fixed `u16`. This is forward-compatible:
237// the underlying type can grow beyond `u16` in the future without breaking
238// the wire format. The encoding uses 1, 2, 4, or 8 bytes depending on
239// magnitude — current IDs (`0x8000-0xFFFF`) take 4 bytes.
240
241/// Number of bytes a QUIC variable-length integer needs to encode `value`.
242///
243/// We delegate to `tls_codec::vlen::write_length` against a fixed-size stack
244/// buffer rather than reimplementing the size table — that way our sizing
245/// can never drift from what `tls_codec` actually emits, even if upstream
246/// boundaries ever change. The 8-byte buffer is the maximum any QUIC vlen
247/// encoding requires (RFC 9000 §16), so the write cannot fail with
248/// `EndOfBuffer`.
249fn 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
262// implements: META-005
263impl 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
269// implements: META-005
270impl 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    // An application name equal to one of these is refused at backend
304    // startup, so the table must hold every META section 2 name exactly, and
305    // only those.
306    #[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        // A new well-known ID gets an event name; it must get a table row too.
323        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        // Hardcoded
334        assert!(ComponentId::COMPONENT_REGISTRY.is_hardcoded());
335        assert!(ComponentId::SUPER_ADMIN_LIST.is_hardcoded());
336        assert!(!ComponentId::ADMIN_LIST.is_hardcoded());
337
338        // Constrained
339        assert!(ComponentId::ADMIN_LIST.is_constrained());
340        assert!(!ComponentId::GROUP_NAME.is_constrained());
341
342        // Mutable XMTP
343        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        // Immutable XMTP
357        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        // XMTP mutable (just after hardcoded)
369        assert!(ComponentId::new(0x8003).is_xmtp_range());
370        assert!(!ComponentId::new(0x8003).is_immutable());
371
372        // The newest mutable XMTP IDs sit just past APP_DATA at 0x800A through 0x800B.
373        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        // XMTP immutable boundary
379        assert!(!ComponentId::new(0xBDFF).is_immutable());
380        assert!(ComponentId::new(0xBE00).is_immutable());
381        assert!(ComponentId::new(0xBFFF).is_immutable());
382
383        // The newest immutable XMTP IDs (DM_MEMBERS, ONESHOT_MESSAGE) sit
384        // counting down from 0xBFFF and must fall inside the immutable subrange.
385        assert!(ComponentId::new(0xBFFD).is_immutable());
386        assert!(ComponentId::new(0xBFFC).is_immutable());
387
388        // App mutable
389        assert!(ComponentId::new(0xC000).is_app_range());
390        assert!(!ComponentId::new(0xC000).is_immutable());
391
392        // App immutable boundary
393        assert!(!ComponentId::new(0xFCFF).is_immutable());
394        assert!(ComponentId::new(0xFD00).is_immutable());
395        assert!(ComponentId::new(0xFEFF).is_immutable());
396
397        // Reserved
398        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        // XMTP and App ranges don't overlap
414        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        // Round-trip every well-known id and a few representative values from
437        // the various ranges. Component IDs are encoded with QUIC vlen so the
438        // serialized length depends on magnitude.
439        let ids = [
440            ComponentId::new(0x0000),
441            ComponentId::new(0x003F), // last 1-byte vlen value
442            ComponentId::new(0x0040), // first 2-byte vlen value
443            ComponentId::new(0x3FFF), // last 2-byte vlen value
444            ComponentId::new(0x4000), // first 4-byte vlen value
445            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    // verifies: META-005
468    fn test_vlen_encoding_sizes() {
469        // 1-byte vlen: 0..=0x3F
470        assert_eq!(ComponentId::new(0).tls_serialized_len(), 1);
471        assert_eq!(ComponentId::new(0x3F).tls_serialized_len(), 1);
472        // 2-byte vlen: 0x40..=0x3FFF
473        assert_eq!(ComponentId::new(0x40).tls_serialized_len(), 2);
474        assert_eq!(ComponentId::new(0x3FFF).tls_serialized_len(), 2);
475        // 4-byte vlen: 0x4000..
476        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    // verifies: META-005
483    fn test_decode_rejects_value_above_u16_max() {
484        // A QUIC vlen-encoded value of 0x10000 (one past u16::MAX) takes 4
485        // bytes: prefix 0b10 (4-byte) || 0x00_01_00_00.
486        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}