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 ---
24const APP_RANGE_START: u16 = 0xC000;
25const APP_RANGE_END: u16 = 0xFEFF;
26/// Immutable application components: 0xFD00-0xFEFF (512 IDs, counting down).
27const APP_IMMUTABLE_START: u16 = 0xFD00;
28
29// --- Reserved: 0xFF00-0xFFFF ---
30const COMPONENT_RESERVED_START: u16 = 0xFF00;
31
32// Compile-time invariant checks: if any of the range constants are ever
33// changed in a way that breaks these assumptions, this will fail to build.
34// The validation logic in `ComponentRegistry` relies on these — the ranges
35// must be contiguous, non-overlapping, and immutable subranges must sit at
36// the end of their parent block.
37const _RANGE_INVARIANTS: () = {
38    // The component space starts at exactly the XMTP range.
39    assert!(XMTP_RANGE_START == COMPONENT_RANGE_START);
40    assert!(XMTP_RANGE_START < XMTP_RANGE_END);
41    // XMTP, App, and Reserved are contiguous and non-overlapping.
42    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    // Immutable subranges sit strictly inside their parent block, at the end.
46    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/// A component identifier in the XMTP app data system.
53///
54/// ComponentIds occupy the top half of the u16 space (`0x8000-0xFFFF`), split
55/// between XMTP protocol use (`0x8000-0xBFFF`) and application-defined
56/// components (`0xC000-0xFEFF`), with `0xFF00-0xFFFF` reserved.
57///
58/// Immutable ranges sit at the end of each block (last 512 IDs, counting down).
59/// Components in these ranges can be written once but never updated or deleted.
60#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct ComponentId(u16);
62
63impl ComponentId {
64    // === Hardcoded Component IDs ===
65    // Permissions for these are enforced in code, not in the permissions map.
66
67    /// The component registry. Super admin only.
68    pub const COMPONENT_REGISTRY: Self = Self(0x8000);
69    /// The super admin list. Super admin only.
70    pub const SUPER_ADMIN_LIST: Self = Self(0x8001);
71    /// The admin list. Configurable: super admin only or admin/super admin.
72    pub const ADMIN_LIST: Self = Self(0x8002);
73
74    // === Well-Known Mutable XMTP Component IDs (counting up from 0x8003) ===
75
76    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    // === Well-Known Immutable XMTP Component IDs (counting down from 0xBFFF) ===
87
88    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    // === Constructor and Accessors ===
94
95    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    // === Range Helpers ===
104
105    /// Returns true if the ID is in the component ID space (top half of u16).
106    /// Note: this includes the reserved range (`0xFF00-0xFFFF`); use
107    /// [`is_reserved`](Self::is_reserved) to distinguish.
108    pub const fn is_in_component_space(self) -> bool {
109        self.0 >= COMPONENT_RANGE_START
110    }
111
112    /// Returns true if the ID is in the XMTP protocol range (`0x8000-0xBFFF`).
113    pub const fn is_xmtp_range(self) -> bool {
114        self.0 >= XMTP_RANGE_START && self.0 <= XMTP_RANGE_END
115    }
116
117    /// Returns true if the ID is in the application range (`0xC000-0xFEFF`).
118    pub const fn is_app_range(self) -> bool {
119        self.0 >= APP_RANGE_START && self.0 <= APP_RANGE_END
120    }
121
122    /// Returns true if the ID is in the reserved range (`0xFF00-0xFFFF`).
123    pub const fn is_reserved(self) -> bool {
124        self.0 >= COMPONENT_RESERVED_START
125    }
126
127    /// Returns true if the ID is in an immutable range.
128    /// Immutable components can be inserted once but never updated or deleted.
129    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    /// Returns true if this is one of the hardcoded components whose
135    /// permissions are enforced in code rather than the component registry.
136    pub const fn is_hardcoded(self) -> bool {
137        self.0 == Self::COMPONENT_REGISTRY.0 || self.0 == Self::SUPER_ADMIN_LIST.0
138    }
139
140    /// Returns true if this component has constrained permission values.
141    /// Constrained components can only have their permissions set to the
142    /// proto base policies `AllowIfAdmin` (admin or super admin) or
143    /// `AllowIfSuperAdmin` (super admin only).
144    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
173// === TLS Codec ===
174//
175// ComponentIds are encoded using QUIC variable-length integer encoding
176// (RFC 9000 §16) rather than a fixed `u16`. This is forward-compatible:
177// the underlying type can grow beyond `u16` in the future without breaking
178// the wire format. The encoding uses 1, 2, 4, or 8 bytes depending on
179// magnitude — current IDs (`0x8000-0xFFFF`) take 4 bytes.
180
181/// Number of bytes a QUIC variable-length integer needs to encode `value`.
182///
183/// We delegate to `tls_codec::vlen::write_length` against a fixed-size stack
184/// buffer rather than reimplementing the size table — that way our sizing
185/// can never drift from what `tls_codec` actually emits, even if upstream
186/// boundaries ever change. The 8-byte buffer is the maximum any QUIC vlen
187/// encoding requires (RFC 9000 §16), so the write cannot fail with
188/// `EndOfBuffer`.
189fn 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        // Hardcoded
232        assert!(ComponentId::COMPONENT_REGISTRY.is_hardcoded());
233        assert!(ComponentId::SUPER_ADMIN_LIST.is_hardcoded());
234        assert!(!ComponentId::ADMIN_LIST.is_hardcoded());
235
236        // Constrained
237        assert!(ComponentId::ADMIN_LIST.is_constrained());
238        assert!(!ComponentId::GROUP_NAME.is_constrained());
239
240        // Mutable XMTP
241        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        // Immutable XMTP
251        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        // XMTP mutable (just after hardcoded)
263        assert!(ComponentId::new(0x8003).is_xmtp_range());
264        assert!(!ComponentId::new(0x8003).is_immutable());
265
266        // The newest mutable XMTP IDs sit just past APP_DATA at 0x800A and 0x800B.
267        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        // XMTP immutable boundary
273        assert!(!ComponentId::new(0xBDFF).is_immutable());
274        assert!(ComponentId::new(0xBE00).is_immutable());
275        assert!(ComponentId::new(0xBFFF).is_immutable());
276
277        // The newest immutable XMTP IDs (DM_MEMBERS, ONESHOT_MESSAGE) sit
278        // counting down from 0xBFFF and must fall inside the immutable subrange.
279        assert!(ComponentId::new(0xBFFD).is_immutable());
280        assert!(ComponentId::new(0xBFFC).is_immutable());
281
282        // App mutable
283        assert!(ComponentId::new(0xC000).is_app_range());
284        assert!(!ComponentId::new(0xC000).is_immutable());
285
286        // App immutable boundary
287        assert!(!ComponentId::new(0xFCFF).is_immutable());
288        assert!(ComponentId::new(0xFD00).is_immutable());
289        assert!(ComponentId::new(0xFEFF).is_immutable());
290
291        // Reserved
292        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        // XMTP and App ranges don't overlap
308        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        // Round-trip every well-known id and a few representative values from
331        // the various ranges. Component IDs are encoded with QUIC vlen so the
332        // serialized length depends on magnitude.
333        let ids = [
334            ComponentId::new(0x0000),
335            ComponentId::new(0x003F), // last 1-byte vlen value
336            ComponentId::new(0x0040), // first 2-byte vlen value
337            ComponentId::new(0x3FFF), // last 2-byte vlen value
338            ComponentId::new(0x4000), // first 4-byte vlen value
339            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        // 1-byte vlen: 0..=0x3F
363        assert_eq!(ComponentId::new(0).tls_serialized_len(), 1);
364        assert_eq!(ComponentId::new(0x3F).tls_serialized_len(), 1);
365        // 2-byte vlen: 0x40..=0x3FFF
366        assert_eq!(ComponentId::new(0x40).tls_serialized_len(), 2);
367        assert_eq!(ComponentId::new(0x3FFF).tls_serialized_len(), 2);
368        // 4-byte vlen: 0x4000..
369        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        // A QUIC vlen-encoded value of 0x10000 (one past u16::MAX) takes 4
377        // bytes: prefix 0b10 (4-byte) || 0x00_01_00_00.
378        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}