Skip to main content

xmtp_proto/types/ids/
group_id.rs

1use std::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
2
3#[cfg(feature = "diesel")]
4use diesel::{
5    backend::Backend,
6    deserialize::{self, FromSql, FromSqlRow},
7    expression::AsExpression,
8    serialize::{self, IsNull, Output, ToSql},
9    sql_types::Binary,
10    sqlite::Sqlite,
11};
12use serde::{Deserialize, Serialize};
13
14use crate::ConversionError;
15
16/// The canonical group identifier. Exactly 16 bytes, by protocol invariant.
17#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
18#[cfg_attr(feature = "diesel", derive(AsExpression, FromSqlRow))]
19#[cfg_attr(feature = "diesel", diesel(sql_type = Binary))]
20pub struct GroupId([u8; 16]);
21
22impl GroupId {
23    /// `GroupId([0u8; 16])` — sentinel / placeholder. Same as `GroupId::default()`.
24    pub const ZERO: GroupId = GroupId([0u8; 16]);
25    /// `GroupId([1u8; 16])` — convenience constant for tests.
26    pub const ONE: GroupId = GroupId([1u8; 16]);
27    /// `GroupId([2u8; 16])` — convenience constant for tests.
28    pub const TWO: GroupId = GroupId([2u8; 16]);
29    /// `GroupId([3u8; 16])` — convenience constant for tests.
30    pub const THREE: GroupId = GroupId([3u8; 16]);
31    /// `GroupId([4u8; 16])` — convenience constant for tests.
32    pub const FOUR: GroupId = GroupId([4u8; 16]);
33
34    /// Borrowed byte slice view over the underlying 16 bytes.
35    pub fn as_slice(&self) -> &[u8] {
36        &self.0
37    }
38
39    /// Borrowed reference to the underlying 16-byte array.
40    pub fn as_bytes(&self) -> &[u8; 16] {
41        &self.0
42    }
43
44    /// Consume the `GroupId` and return its raw bytes.
45    pub fn into_bytes(self) -> [u8; 16] {
46        self.0
47    }
48
49    /// Convert to an owned `Vec<u8>` of the 16 bytes.
50    pub fn to_vec(self) -> Vec<u8> {
51        self.0.to_vec()
52    }
53
54    /// Convert to an `openmls::group::GroupId`.
55    pub fn to_openmls(self) -> openmls::group::GroupId {
56        openmls::group::GroupId::from_slice(&self.0)
57    }
58
59    /// Construct a `GroupId` containing 16 random bytes drawn from `rand`.
60    pub fn random<R: openmls_traits::random::OpenMlsRand>(rand: &R) -> Self {
61        let mut bytes = [0u8; 16];
62        let v = rand
63            .random_vec(16)
64            .expect("OpenMlsRand failed to produce randomness for GroupId");
65        bytes.copy_from_slice(&v);
66        GroupId(bytes)
67    }
68}
69
70// a fixed slice [T; N] implements deref for &[u8], so by
71// impl deref on teh [T; N] we also get &[u8] deref for free.
72impl Deref for GroupId {
73    type Target = [u8; 16];
74
75    fn deref(&self) -> &Self::Target {
76        &self.0
77    }
78}
79
80impl<T> AsRef<T> for GroupId
81where
82    T: ?Sized,
83    <GroupId as Deref>::Target: AsRef<T>,
84{
85    fn as_ref(&self) -> &T {
86        self.deref().as_ref()
87    }
88}
89
90// --- Infallible constructors -------------------------------------------------
91
92impl From<[u8; 16]> for GroupId {
93    fn from(v: [u8; 16]) -> Self {
94        GroupId(v)
95    }
96}
97
98impl From<&[u8; 16]> for GroupId {
99    fn from(v: &[u8; 16]) -> Self {
100        GroupId(*v)
101    }
102}
103
104impl From<GroupId> for [u8; 16] {
105    fn from(v: GroupId) -> Self {
106        v.0
107    }
108}
109
110// --- Fallible constructors ---------------------------------------------------
111
112impl TryFrom<Vec<u8>> for GroupId {
113    type Error = ConversionError;
114    fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
115        Ok(GroupId(v.as_slice().try_into()?))
116    }
117}
118
119impl TryFrom<&[u8]> for GroupId {
120    type Error = ConversionError;
121    fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
122        let bytes: [u8; 16] = v.try_into()?;
123        Ok(GroupId(bytes))
124    }
125}
126
127impl TryFrom<&openmls::group::GroupId> for GroupId {
128    type Error = ConversionError;
129    fn try_from(id: &openmls::group::GroupId) -> Result<Self, Self::Error> {
130        GroupId::try_from(id.as_slice())
131    }
132}
133
134impl TryFrom<openmls::group::GroupId> for GroupId {
135    type Error = ConversionError;
136    fn try_from(id: openmls::group::GroupId) -> Result<Self, Self::Error> {
137        GroupId::try_from(id.as_slice())
138    }
139}
140
141// --- Outward conversions -----------------------------------------------------
142
143impl From<GroupId> for Vec<u8> {
144    fn from(id: GroupId) -> Vec<u8> {
145        id.0.to_vec()
146    }
147}
148
149impl Borrow<[u8; 16]> for GroupId {
150    fn borrow(&self) -> &[u8; 16] {
151        &self.0
152    }
153}
154
155// --- Display / Debug ---------------------------------------------------------
156
157impl fmt::Display for GroupId {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "{}", hex::encode(self.0))
160    }
161}
162
163impl fmt::Debug for GroupId {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_tuple("GroupId")
166            .field(&xmtp_common::fmt::debug_hex(self.0))
167            .finish()
168    }
169}
170
171// --- FromStr / parse error ---------------------------------------------------
172
173/// Error returned by `<GroupId as FromStr>::from_str`.
174#[derive(Debug, thiserror::Error)]
175pub enum GroupIdParseError {
176    /// Input string was not valid hexadecimal.
177    #[error(transparent)]
178    Hex(#[from] hex::FromHexError),
179    /// Decoded byte length was not 16.
180    #[error(transparent)]
181    Length(#[from] ConversionError),
182}
183
184impl FromStr for GroupId {
185    type Err = GroupIdParseError;
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        let bytes = hex::decode(s)?;
188        Ok(GroupId::try_from(bytes)?)
189    }
190}
191
192// --- PartialEq family --------------------------------------------------------
193
194impl PartialEq<Vec<u8>> for GroupId {
195    fn eq(&self, other: &Vec<u8>) -> bool {
196        self.0.eq(&other[..])
197    }
198}
199
200impl PartialEq<GroupId> for Vec<u8> {
201    fn eq(&self, other: &GroupId) -> bool {
202        other.0.eq(&self[..])
203    }
204}
205
206impl PartialEq<&Vec<u8>> for GroupId {
207    fn eq(&self, other: &&Vec<u8>) -> bool {
208        self.0.eq(&other[..])
209    }
210}
211
212impl PartialEq<GroupId> for &Vec<u8> {
213    fn eq(&self, other: &GroupId) -> bool {
214        other.0.eq(&self[..])
215    }
216}
217
218impl PartialEq<[u8]> for GroupId {
219    fn eq(&self, other: &[u8]) -> bool {
220        self.0.eq(other)
221    }
222}
223
224impl PartialEq<GroupId> for [u8] {
225    fn eq(&self, other: &GroupId) -> bool {
226        other.0.eq(self)
227    }
228}
229
230impl PartialEq<[u8; 16]> for GroupId {
231    fn eq(&self, other: &[u8; 16]) -> bool {
232        self.0.eq(other)
233    }
234}
235
236impl PartialEq<GroupId> for [u8; 16] {
237    fn eq(&self, other: &GroupId) -> bool {
238        other.0.eq(&self[..])
239    }
240}
241
242// --- Serde -------------------------------------------------------------------
243
244impl Serialize for GroupId {
245    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
246        self.0.as_ref().serialize(s)
247    }
248}
249
250impl<'de> Deserialize<'de> for GroupId {
251    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
252        let v = Vec::<u8>::deserialize(d)?;
253        GroupId::try_from(v).map_err(serde::de::Error::custom)
254    }
255}
256
257// --- Diesel ------------------------------------------------------------------
258
259#[cfg(feature = "diesel")]
260impl ToSql<Binary, Sqlite> for GroupId
261where
262    [u8]: ToSql<Binary, Sqlite>,
263{
264    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
265        out.set_value(self.0.to_vec());
266        Ok(IsNull::No)
267    }
268}
269
270#[cfg(feature = "diesel")]
271impl FromSql<Binary, Sqlite> for GroupId
272where
273    Vec<u8>: FromSql<Binary, Sqlite>,
274{
275    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
276        let v = Vec::<u8>::from_sql(bytes)?;
277        GroupId::try_from(v).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
278    }
279}
280
281// --- Generate (test-only) ----------------------------------------------------
282
283xmtp_common::if_test! {
284    impl xmtp_common::Generate for GroupId {
285        fn generate() -> Self {
286            GroupId(xmtp_common::rand_array::<16>())
287        }
288    }
289}
290
291// --- Tests -------------------------------------------------------------------
292
293#[cfg(test)]
294mod test {
295    use rstest::rstest;
296
297    use super::*;
298
299    #[rstest]
300    #[case([0u8; 16])]
301    #[case([0xffu8; 16])]
302    #[case([1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])]
303    #[xmtp_common::test(unwrap_try = true)]
304    async fn test_group_id_from_array(#[case] input: [u8; 16]) {
305        let id = GroupId::from(input);
306        assert_eq!(id.as_slice(), &input);
307        assert_eq!(id.as_bytes(), &input);
308        assert_eq!(id.into_bytes(), input);
309        assert_eq!(GroupId::from(&input).as_slice(), &input);
310        assert_eq!(GroupId::from(input).to_vec(), input.to_vec());
311    }
312
313    #[rstest]
314    #[case(vec![1u8; 16], true)]
315    #[case(vec![1u8; 15], false)]
316    #[case(vec![1u8; 17], false)]
317    #[case(Vec::new(), false)]
318    #[xmtp_common::test(unwrap_try = true)]
319    async fn test_group_id_try_from_vec(#[case] input: Vec<u8>, #[case] ok: bool) {
320        assert_eq!(GroupId::try_from(input).is_ok(), ok);
321    }
322
323    #[rstest]
324    #[case(&[1u8; 16][..], true)]
325    #[case(&[1u8; 15][..], false)]
326    #[case(&[1u8; 17][..], false)]
327    #[case(&[][..], false)]
328    #[xmtp_common::test(unwrap_try = true)]
329    async fn test_group_id_try_from_slice(#[case] input: &[u8], #[case] ok: bool) {
330        assert_eq!(GroupId::try_from(input).is_ok(), ok);
331    }
332
333    #[xmtp_common::test(unwrap_try = true)]
334    fn test_openmls_try_from_valid() {
335        let bytes: [u8; 16] = xmtp_common::rand_array::<16>();
336        let ommls = openmls::group::GroupId::from_slice(&bytes);
337        let xmtp_id = GroupId::try_from(&ommls).unwrap();
338        assert_eq!(xmtp_id.as_slice(), &bytes);
339
340        let ommls_owned = openmls::group::GroupId::from_slice(&bytes);
341        let xmtp_id_owned = GroupId::try_from(ommls_owned).unwrap();
342        assert_eq!(xmtp_id_owned.as_slice(), &bytes);
343    }
344
345    #[xmtp_common::test(unwrap_try = true)]
346    fn test_openmls_try_from_wrong_length() {
347        let short = openmls::group::GroupId::from_slice(&[1u8; 8]);
348        assert!(GroupId::try_from(&short).is_err());
349
350        let long = openmls::group::GroupId::from_slice(&[1u8; 32]);
351        assert!(GroupId::try_from(long).is_err());
352    }
353
354    #[xmtp_common::test(unwrap_try = true)]
355    fn test_to_openmls_roundtrip() {
356        let bytes: [u8; 16] = xmtp_common::rand_array::<16>();
357        let xmtp_id = GroupId::from(bytes);
358        let ommls_id = xmtp_id.to_openmls();
359        assert_eq!(ommls_id.as_slice(), xmtp_id.as_slice());
360    }
361
362    #[xmtp_common::test(unwrap_try = true)]
363    fn test_fromstr_success() {
364        let bytes: [u8; 16] = xmtp_common::rand_array::<16>();
365        let hex = hex::encode(bytes);
366        let id: GroupId = hex.parse().unwrap();
367        assert_eq!(id.as_slice(), &bytes);
368    }
369
370    #[xmtp_common::test(unwrap_try = true)]
371    fn test_fromstr_bad_hex() {
372        let err = "zz".parse::<GroupId>().unwrap_err();
373        assert!(matches!(err, GroupIdParseError::Hex(_)));
374    }
375
376    #[xmtp_common::test(unwrap_try = true)]
377    fn test_fromstr_wrong_length() {
378        // 6 hex chars = 3 bytes (not 16).
379        let err = "abcdef".parse::<GroupId>().unwrap_err();
380        assert!(matches!(err, GroupIdParseError::Length(_)));
381    }
382
383    #[rstest]
384    #[case(GroupId::from([1u8; 16]), vec![1u8; 16], true)]
385    #[case(GroupId::from([1u8; 16]), vec![2u8; 16], false)]
386    #[case(GroupId::from([1u8; 16]), vec![1u8; 15], false)] // length mismatch
387    #[xmtp_common::test(unwrap_try = true)]
388    async fn test_group_id_eq_vec(#[case] id: GroupId, #[case] v: Vec<u8>, #[case] equal: bool) {
389        // Each direction exercises a different PartialEq impl: by-value, by-reference,
390        // and the reverse pair. The op_ref allow keeps the &v cases intentional.
391        #[allow(clippy::op_ref)]
392        {
393            assert_eq!(id == v, equal);
394            assert_eq!(v == id, equal);
395            assert_eq!(id == &v, equal);
396            assert_eq!(&v == id, equal);
397        }
398    }
399
400    #[rstest]
401    #[case(GroupId::from([1u8; 16]), [1u8; 16], true)]
402    #[case(GroupId::from([1u8; 16]), [2u8; 16], false)]
403    #[xmtp_common::test(unwrap_try = true)]
404    async fn test_group_id_eq_array(#[case] id: GroupId, #[case] a: [u8; 16], #[case] equal: bool) {
405        assert_eq!(id == a, equal);
406        assert_eq!(a == id, equal);
407    }
408
409    #[xmtp_common::test(unwrap_try = true)]
410    async fn test_group_id_eq_slice() {
411        let id = GroupId::from([1u8; 16]);
412        let s: &[u8] = &[1u8; 16];
413        assert_eq!(id, *s);
414        assert_eq!(*s, id);
415
416        let wrong_len: &[u8] = &[1u8; 8];
417        assert_ne!(id, *wrong_len);
418        assert_ne!(*wrong_len, id);
419    }
420
421    #[xmtp_common::test(unwrap_try = true)]
422    fn test_serde_roundtrip() {
423        let id = GroupId::from([7u8; 16]);
424        let bytes = bincode::serialize(&id).unwrap();
425        let decoded: GroupId = bincode::deserialize(&bytes).unwrap();
426        assert_eq!(decoded, id);
427    }
428
429    #[xmtp_common::test(unwrap_try = true)]
430    fn test_serde_wrong_length_fails() {
431        // Manually craft a bincode-encoded Vec<u8> of length 8.
432        let bad: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8];
433        let bytes = bincode::serialize(&bad).unwrap();
434        assert!(bincode::deserialize::<GroupId>(&bytes).is_err());
435    }
436
437    #[xmtp_common::test(unwrap_try = true)]
438    fn test_generate_produces_16_bytes() {
439        use xmtp_common::Generate;
440        let id: GroupId = GroupId::generate();
441        assert_eq!(id.as_slice().len(), 16);
442    }
443
444    #[xmtp_common::test(unwrap_try = true)]
445    fn test_default_is_zero() {
446        let id = GroupId::default();
447        assert_eq!(id.as_slice(), &[0u8; 16][..]);
448    }
449
450    #[xmtp_common::test(unwrap_try = true)]
451    fn test_const_helpers() {
452        assert_eq!(GroupId::ZERO, GroupId::default());
453        assert_eq!(GroupId::ZERO.as_bytes(), &[0u8; 16]);
454        assert_eq!(GroupId::ONE.as_bytes(), &[1u8; 16]);
455        assert_eq!(GroupId::TWO.as_bytes(), &[2u8; 16]);
456        assert_eq!(GroupId::THREE.as_bytes(), &[3u8; 16]);
457        assert_eq!(GroupId::FOUR.as_bytes(), &[4u8; 16]);
458    }
459
460    #[xmtp_common::test(unwrap_try = true)]
461    fn test_display_debug() {
462        let id = GroupId::from([0x12, 0x34, 0xab, 0xcd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
463        let displayed = format!("{}", id);
464        assert!(displayed.starts_with("1234abcd"));
465        assert_eq!(displayed.len(), 32);
466        let debugged = format!("{:?}", id);
467        assert!(debugged.starts_with("GroupId("));
468    }
469
470    #[cfg(feature = "diesel")]
471    mod diesel_test {
472        use super::*;
473        use diesel::prelude::*;
474
475        diesel::table! {
476            test_group_ids (id) {
477                id -> Binary,
478            }
479        }
480
481        #[derive(Insertable, Queryable)]
482        #[diesel(table_name = test_group_ids)]
483        struct Row {
484            id: GroupId,
485        }
486
487        #[xmtp_common::test(unwrap_try = true)]
488        fn test_diesel_roundtrip() {
489            let mut conn = SqliteConnection::establish(":memory:").unwrap();
490            diesel::sql_query("CREATE TABLE test_group_ids (id BLOB NOT NULL PRIMARY KEY)")
491                .execute(&mut conn)
492                .unwrap();
493            let id = GroupId::from([0xabu8; 16]);
494            diesel::insert_into(test_group_ids::table)
495                .values(&Row { id })
496                .execute(&mut conn)
497                .unwrap();
498            let got: Row = test_group_ids::table.first(&mut conn).unwrap();
499            assert_eq!(got.id, id);
500        }
501
502        #[xmtp_common::test(unwrap_try = true)]
503        fn test_diesel_wrong_length_errors() {
504            let mut conn = SqliteConnection::establish(":memory:").unwrap();
505            diesel::sql_query("CREATE TABLE test_group_ids (id BLOB NOT NULL PRIMARY KEY)")
506                .execute(&mut conn)
507                .unwrap();
508            // Insert raw 8-byte blob bypassing the type.
509            diesel::sql_query("INSERT INTO test_group_ids (id) VALUES (X'0102030405060708')")
510                .execute(&mut conn)
511                .unwrap();
512            let result: Result<Row, _> = test_group_ids::table.first(&mut conn);
513            assert!(result.is_err());
514        }
515    }
516}