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;
15use xmtp_configuration::{GROUP_ID_BYTES, LEGACY_GROUP_ID_BYTES};
16
17/// The canonical group identifier: 32 bytes for a keyed group, whose id is the
18/// public key of its group signing key, or 16 bytes for a legacy group known
19/// only from imported archives.
20///
21/// Equality, ordering, and hashing use the id bytes, exactly as for `[u8]`.
22#[derive(Copy, Clone)]
23#[cfg_attr(feature = "diesel", derive(AsExpression, FromSqlRow))]
24#[cfg_attr(feature = "diesel", diesel(sql_type = Binary))]
25pub struct GroupId {
26    /// `LEGACY_GROUP_ID_BYTES` or `GROUP_ID_BYTES`.
27    len: u8,
28    /// The id in the first `len` bytes; the rest are zero.
29    bytes: [u8; GROUP_ID_BYTES],
30}
31
32const fn legacy(byte: u8) -> GroupId {
33    let mut bytes = [0u8; GROUP_ID_BYTES];
34    let mut i = 0;
35    while i < LEGACY_GROUP_ID_BYTES {
36        bytes[i] = byte;
37        i += 1;
38    }
39    GroupId {
40        len: LEGACY_GROUP_ID_BYTES as u8,
41        bytes,
42    }
43}
44
45impl GroupId {
46    /// The 16-byte legacy id of zeros: a sentinel / placeholder. Same as `GroupId::default()`.
47    pub const ZERO: GroupId = legacy(0);
48    /// The 16-byte legacy id of ones, for tests.
49    pub const ONE: GroupId = legacy(1);
50    /// The 16-byte legacy id of twos, for tests.
51    pub const TWO: GroupId = legacy(2);
52    /// The 16-byte legacy id of threes, for tests.
53    pub const THREE: GroupId = legacy(3);
54    /// The 16-byte legacy id of fours, for tests.
55    pub const FOUR: GroupId = legacy(4);
56
57    fn from_slice(v: &[u8]) -> Result<Self, ConversionError> {
58        if v.len() != GROUP_ID_BYTES && v.len() != LEGACY_GROUP_ID_BYTES {
59            return Err(ConversionError::InvalidValue {
60                item: "GroupId",
61                expected: "a 32-byte or 16-byte group id",
62                got: format!("{} bytes", v.len()),
63            });
64        }
65        let mut bytes = [0u8; GROUP_ID_BYTES];
66        bytes[..v.len()].copy_from_slice(v);
67        Ok(GroupId {
68            len: v.len() as u8,
69            bytes,
70        })
71    }
72
73    /// Borrowed byte slice view over the id bytes.
74    pub fn as_slice(&self) -> &[u8] {
75        &self.bytes[..self.len as usize]
76    }
77
78    /// The id length in bytes: 32, or 16 for a legacy group.
79    #[allow(clippy::len_without_is_empty)]
80    pub fn len(&self) -> usize {
81        self.len as usize
82    }
83
84    /// Whether this is a 16-byte legacy group id.
85    pub fn is_legacy(&self) -> bool {
86        self.len() == LEGACY_GROUP_ID_BYTES
87    }
88
89    /// Convert to an owned `Vec<u8>` of the id bytes.
90    pub fn to_vec(self) -> Vec<u8> {
91        self.as_slice().to_vec()
92    }
93
94    /// Convert to an `openmls::group::GroupId`.
95    pub fn to_openmls(self) -> openmls::group::GroupId {
96        openmls::group::GroupId::from_slice(self.as_slice())
97    }
98
99    /// Construct a 16-byte legacy `GroupId` from random bytes drawn from `rand`.
100    pub fn random<R: openmls_traits::random::OpenMlsRand>(rand: &R) -> Self {
101        let v = rand
102            .random_vec(LEGACY_GROUP_ID_BYTES)
103            .expect("OpenMlsRand failed to produce randomness for GroupId");
104        GroupId::from_slice(&v).expect("random_vec returns the requested length")
105    }
106}
107
108impl Default for GroupId {
109    fn default() -> Self {
110        GroupId::ZERO
111    }
112}
113
114impl PartialEq for GroupId {
115    fn eq(&self, other: &Self) -> bool {
116        self.as_slice() == other.as_slice()
117    }
118}
119
120impl Eq for GroupId {}
121
122impl PartialOrd for GroupId {
123    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
124        Some(self.cmp(other))
125    }
126}
127
128impl Ord for GroupId {
129    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
130        self.as_slice().cmp(other.as_slice())
131    }
132}
133
134// Hashes as `[u8]`, so `Borrow<[u8]>` lookups find the same entry.
135impl std::hash::Hash for GroupId {
136    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
137        self.as_slice().hash(state)
138    }
139}
140
141impl Deref for GroupId {
142    type Target = [u8];
143
144    fn deref(&self) -> &Self::Target {
145        self.as_slice()
146    }
147}
148
149impl AsRef<[u8]> for GroupId {
150    fn as_ref(&self) -> &[u8] {
151        self.as_slice()
152    }
153}
154
155// --- Infallible constructors -------------------------------------------------
156
157impl From<[u8; LEGACY_GROUP_ID_BYTES]> for GroupId {
158    fn from(v: [u8; LEGACY_GROUP_ID_BYTES]) -> Self {
159        GroupId::from(&v)
160    }
161}
162
163impl From<&[u8; LEGACY_GROUP_ID_BYTES]> for GroupId {
164    fn from(v: &[u8; LEGACY_GROUP_ID_BYTES]) -> Self {
165        let mut bytes = [0u8; GROUP_ID_BYTES];
166        bytes[..LEGACY_GROUP_ID_BYTES].copy_from_slice(v);
167        GroupId {
168            len: LEGACY_GROUP_ID_BYTES as u8,
169            bytes,
170        }
171    }
172}
173
174impl From<[u8; GROUP_ID_BYTES]> for GroupId {
175    fn from(bytes: [u8; GROUP_ID_BYTES]) -> Self {
176        GroupId {
177            len: GROUP_ID_BYTES as u8,
178            bytes,
179        }
180    }
181}
182
183impl From<&[u8; GROUP_ID_BYTES]> for GroupId {
184    fn from(v: &[u8; GROUP_ID_BYTES]) -> Self {
185        GroupId::from(*v)
186    }
187}
188
189// --- Fallible constructors ---------------------------------------------------
190
191impl TryFrom<Vec<u8>> for GroupId {
192    type Error = ConversionError;
193    fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
194        GroupId::from_slice(&v)
195    }
196}
197
198impl TryFrom<&[u8]> for GroupId {
199    type Error = ConversionError;
200    fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
201        GroupId::from_slice(v)
202    }
203}
204
205impl TryFrom<&openmls::group::GroupId> for GroupId {
206    type Error = ConversionError;
207    fn try_from(id: &openmls::group::GroupId) -> Result<Self, Self::Error> {
208        GroupId::from_slice(id.as_slice())
209    }
210}
211
212impl TryFrom<openmls::group::GroupId> for GroupId {
213    type Error = ConversionError;
214    fn try_from(id: openmls::group::GroupId) -> Result<Self, Self::Error> {
215        GroupId::from_slice(id.as_slice())
216    }
217}
218
219// --- Outward conversions -----------------------------------------------------
220
221impl From<GroupId> for Vec<u8> {
222    fn from(id: GroupId) -> Vec<u8> {
223        id.to_vec()
224    }
225}
226
227impl Borrow<[u8]> for GroupId {
228    fn borrow(&self) -> &[u8] {
229        self.as_slice()
230    }
231}
232
233// --- Display / Debug ---------------------------------------------------------
234
235impl fmt::Display for GroupId {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        write!(f, "{}", hex::encode(self.as_slice()))
238    }
239}
240
241impl fmt::Debug for GroupId {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        f.debug_tuple("GroupId")
244            .field(&xmtp_common::fmt::debug_hex(self.as_slice()))
245            .finish()
246    }
247}
248
249// --- FromStr / parse error ---------------------------------------------------
250
251/// Error returned by `<GroupId as FromStr>::from_str`.
252#[derive(Debug, thiserror::Error)]
253pub enum GroupIdParseError {
254    /// Input string was not valid hexadecimal.
255    #[error(transparent)]
256    Hex(#[from] hex::FromHexError),
257    /// Decoded byte length was neither 16 nor 32.
258    #[error(transparent)]
259    Length(#[from] ConversionError),
260}
261
262impl FromStr for GroupId {
263    type Err = GroupIdParseError;
264    fn from_str(s: &str) -> Result<Self, Self::Err> {
265        if s.len() != 2 * GROUP_ID_BYTES && s.len() != 2 * LEGACY_GROUP_ID_BYTES {
266            return Err(GroupIdParseError::Length(ConversionError::InvalidValue {
267                item: "GroupId",
268                expected: "32 or 64 hexadecimal characters",
269                got: format!("{} characters", s.len()),
270            }));
271        }
272        let len = s.len() / 2;
273        let mut bytes = [0u8; GROUP_ID_BYTES];
274        hex::decode_to_slice(s, &mut bytes[..len])?;
275        Ok(GroupId::from_slice(&bytes[..len])?)
276    }
277}
278
279// --- PartialEq family --------------------------------------------------------
280
281impl PartialEq<Vec<u8>> for GroupId {
282    fn eq(&self, other: &Vec<u8>) -> bool {
283        self.as_slice() == &other[..]
284    }
285}
286
287impl PartialEq<GroupId> for Vec<u8> {
288    fn eq(&self, other: &GroupId) -> bool {
289        other.as_slice() == &self[..]
290    }
291}
292
293impl PartialEq<&Vec<u8>> for GroupId {
294    fn eq(&self, other: &&Vec<u8>) -> bool {
295        self.as_slice() == &other[..]
296    }
297}
298
299impl PartialEq<GroupId> for &Vec<u8> {
300    fn eq(&self, other: &GroupId) -> bool {
301        other.as_slice() == &self[..]
302    }
303}
304
305impl PartialEq<[u8]> for GroupId {
306    fn eq(&self, other: &[u8]) -> bool {
307        self.as_slice() == other
308    }
309}
310
311impl PartialEq<GroupId> for [u8] {
312    fn eq(&self, other: &GroupId) -> bool {
313        other.as_slice() == self
314    }
315}
316
317impl PartialEq<[u8; LEGACY_GROUP_ID_BYTES]> for GroupId {
318    fn eq(&self, other: &[u8; LEGACY_GROUP_ID_BYTES]) -> bool {
319        self.as_slice() == &other[..]
320    }
321}
322
323impl PartialEq<GroupId> for [u8; LEGACY_GROUP_ID_BYTES] {
324    fn eq(&self, other: &GroupId) -> bool {
325        other.as_slice() == &self[..]
326    }
327}
328
329impl PartialEq<[u8; GROUP_ID_BYTES]> for GroupId {
330    fn eq(&self, other: &[u8; GROUP_ID_BYTES]) -> bool {
331        self.as_slice() == &other[..]
332    }
333}
334
335impl PartialEq<GroupId> for [u8; GROUP_ID_BYTES] {
336    fn eq(&self, other: &GroupId) -> bool {
337        other.as_slice() == &self[..]
338    }
339}
340
341// --- Serde -------------------------------------------------------------------
342
343impl Serialize for GroupId {
344    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
345        self.as_slice().serialize(s)
346    }
347}
348
349impl<'de> Deserialize<'de> for GroupId {
350    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
351        let v = Vec::<u8>::deserialize(d)?;
352        GroupId::try_from(v).map_err(serde::de::Error::custom)
353    }
354}
355
356// --- Diesel ------------------------------------------------------------------
357
358#[cfg(feature = "diesel")]
359impl ToSql<Binary, Sqlite> for GroupId
360where
361    [u8]: ToSql<Binary, Sqlite>,
362{
363    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
364        out.set_value(self.to_vec());
365        Ok(IsNull::No)
366    }
367}
368
369#[cfg(feature = "diesel")]
370impl FromSql<Binary, Sqlite> for GroupId
371where
372    Vec<u8>: FromSql<Binary, Sqlite>,
373{
374    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
375        let v = Vec::<u8>::from_sql(bytes)?;
376        GroupId::try_from(v).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
377    }
378}
379
380// --- Generate (test-only) ----------------------------------------------------
381
382xmtp_common::if_test! {
383    impl xmtp_common::Generate for GroupId {
384        /// A random 32-byte id, the length of every group a client creates.
385        fn generate() -> Self {
386            GroupId::from(xmtp_common::rand_array::<{ GROUP_ID_BYTES }>())
387        }
388    }
389}
390
391// --- Tests -------------------------------------------------------------------
392
393#[cfg(test)]
394mod test {
395    use rstest::rstest;
396
397    use super::*;
398
399    #[rstest]
400    #[case([0u8; 16])]
401    #[case([0xffu8; 16])]
402    #[case([1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])]
403    #[xmtp_common::test(unwrap_try = true)]
404    async fn test_group_id_from_legacy_array(#[case] input: [u8; 16]) {
405        let id = GroupId::from(input);
406        assert_eq!(id.as_slice(), &input);
407        assert_eq!(id.len(), 16);
408        assert!(id.is_legacy());
409        assert_eq!(GroupId::from(&input).as_slice(), &input);
410        assert_eq!(GroupId::from(input).to_vec(), input.to_vec());
411    }
412
413    #[rstest]
414    #[case([0u8; 32])]
415    #[case([0xffu8; 32])]
416    #[xmtp_common::test(unwrap_try = true)]
417    async fn test_group_id_from_keyed_array(#[case] input: [u8; 32]) {
418        let id = GroupId::from(input);
419        assert_eq!(id.as_slice(), &input);
420        assert_eq!(id.len(), 32);
421        assert!(!id.is_legacy());
422        assert_eq!(GroupId::from(&input).as_slice(), &input);
423        assert_eq!(Vec::from(id), input.to_vec());
424    }
425
426    // 16 and 32 bytes convert; every other length fails.
427    #[rstest]
428    #[case(vec![1u8; 16], true)]
429    #[case(vec![1u8; 32], true)]
430    #[case(vec![1u8; 8], false)]
431    #[case(vec![1u8; 15], false)]
432    #[case(vec![1u8; 17], false)]
433    #[case(vec![1u8; 31], false)]
434    #[case(vec![1u8; 33], false)]
435    #[case(Vec::new(), false)]
436    #[xmtp_common::test(unwrap_try = true)]
437    async fn test_group_id_try_from_vec(#[case] input: Vec<u8>, #[case] ok: bool) {
438        assert_eq!(GroupId::try_from(input.clone()).is_ok(), ok);
439        assert_eq!(GroupId::try_from(input.as_slice()).is_ok(), ok);
440        if ok {
441            assert_eq!(
442                GroupId::try_from(input.clone()).unwrap().as_slice(),
443                input.as_slice()
444            );
445        }
446    }
447
448    #[rstest]
449    #[case(16)]
450    #[case(32)]
451    #[xmtp_common::test(unwrap_try = true)]
452    fn test_openmls_roundtrip(#[case] len: usize) {
453        let bytes = xmtp_common::rand_vec::<64>()[..len].to_vec();
454        let ommls = openmls::group::GroupId::from_slice(&bytes);
455        let xmtp_id = GroupId::try_from(&ommls).unwrap();
456        assert_eq!(xmtp_id.as_slice(), &bytes[..]);
457        assert_eq!(GroupId::try_from(ommls).unwrap().as_slice(), &bytes[..]);
458        assert_eq!(xmtp_id.to_openmls().as_slice(), &bytes[..]);
459    }
460
461    #[rstest]
462    #[case(8)]
463    #[case(33)]
464    #[xmtp_common::test(unwrap_try = true)]
465    fn test_openmls_try_from_wrong_length(#[case] len: usize) {
466        let ommls = openmls::group::GroupId::from_slice(&vec![1u8; len]);
467        assert!(GroupId::try_from(&ommls).is_err());
468        assert!(GroupId::try_from(ommls).is_err());
469    }
470
471    #[rstest]
472    #[case(16)]
473    #[case(32)]
474    #[xmtp_common::test(unwrap_try = true)]
475    fn test_hex_roundtrip(#[case] len: usize) {
476        let bytes = xmtp_common::rand_vec::<64>()[..len].to_vec();
477        let hex = hex::encode(&bytes);
478        let id: GroupId = hex.parse().unwrap();
479        assert_eq!(id.as_slice(), &bytes[..]);
480        assert_eq!(id.to_string(), hex);
481    }
482
483    #[xmtp_common::test(unwrap_try = true)]
484    fn test_fromstr_bad_hex() {
485        let err = "zz".repeat(32).parse::<GroupId>().unwrap_err();
486        assert!(matches!(err, GroupIdParseError::Hex(_)));
487    }
488
489    #[rstest]
490    #[case(0)]
491    #[case(16)]
492    #[case(31)]
493    #[case(33)]
494    #[case(65)]
495    #[case(66)]
496    #[xmtp_common::test(unwrap_try = true)]
497    fn test_fromstr_wrong_length(#[case] len: usize) {
498        let err = "a".repeat(len).parse::<GroupId>().unwrap_err();
499        assert!(matches!(err, GroupIdParseError::Length(_)));
500    }
501
502    #[rstest]
503    #[case(GroupId::from([1u8; 16]), vec![1u8; 16], true)]
504    #[case(GroupId::from([1u8; 16]), vec![2u8; 16], false)]
505    #[case(GroupId::from([1u8; 16]), vec![1u8; 15], false)] // length mismatch
506    #[case(GroupId::from([1u8; 16]), vec![1u8; 32], false)] // length mismatch
507    #[case(GroupId::from([1u8; 32]), vec![1u8; 32], true)]
508    #[xmtp_common::test(unwrap_try = true)]
509    async fn test_group_id_eq_vec(#[case] id: GroupId, #[case] v: Vec<u8>, #[case] equal: bool) {
510        // Each direction exercises a different PartialEq impl: by-value, by-reference,
511        // and the reverse pair. The op_ref allow keeps the &v cases intentional.
512        #[allow(clippy::op_ref)]
513        {
514            assert_eq!(id == v, equal);
515            assert_eq!(v == id, equal);
516            assert_eq!(id == &v, equal);
517            assert_eq!(&v == id, equal);
518        }
519    }
520
521    #[rstest]
522    #[case(GroupId::from([1u8; 16]), [1u8; 16], true)]
523    #[case(GroupId::from([1u8; 16]), [2u8; 16], false)]
524    #[xmtp_common::test(unwrap_try = true)]
525    async fn test_group_id_eq_array(#[case] id: GroupId, #[case] a: [u8; 16], #[case] equal: bool) {
526        assert_eq!(id == a, equal);
527        assert_eq!(a == id, equal);
528    }
529
530    #[xmtp_common::test(unwrap_try = true)]
531    async fn test_group_id_eq_slice() {
532        let id = GroupId::from([1u8; 16]);
533        let s: &[u8] = &[1u8; 16];
534        assert_eq!(id, *s);
535        assert_eq!(*s, id);
536
537        let wrong_len: &[u8] = &[1u8; 8];
538        assert_ne!(id, *wrong_len);
539        assert_ne!(*wrong_len, id);
540    }
541
542    /// Equality, order, and hashing follow the id bytes, so a 16-byte id is
543    /// never equal to a 32-byte id that starts with it, and map lookups by
544    /// `&[u8]` find the entry.
545    #[xmtp_common::test(unwrap_try = true)]
546    fn test_eq_ord_hash_follow_bytes() {
547        use std::collections::{BTreeSet, HashMap};
548        let legacy = GroupId::from([0u8; 16]);
549        let keyed = GroupId::from([0u8; 32]);
550        assert_ne!(legacy, keyed);
551        assert!(legacy < keyed);
552        assert!(GroupId::from([1u8; 16]) > keyed);
553        let set: BTreeSet<_> = [keyed, legacy, GroupId::from([1u8; 16])].into();
554        assert_eq!(set.len(), 3);
555        let map = HashMap::from([(legacy, 1), (keyed, 2)]);
556        assert_eq!(map.get(&[0u8; 16][..]), Some(&1));
557        assert_eq!(map.get(&[0u8; 32][..]), Some(&2));
558    }
559
560    /// The client database stores values as CBOR.
561    #[rstest]
562    #[case(GroupId::from([7u8; 16]))]
563    #[case(GroupId::from([7u8; 32]))]
564    #[xmtp_common::test(unwrap_try = true)]
565    fn test_serde_roundtrip(#[case] id: GroupId) {
566        let bytes = cbor4ii::serde::to_vec(Vec::new(), &id).unwrap();
567        let decoded: GroupId = cbor4ii::serde::from_slice(&bytes).unwrap();
568        assert_eq!(decoded, id);
569        assert_eq!(decoded.len(), id.len());
570    }
571
572    #[rstest]
573    #[case(8)]
574    #[case(33)]
575    #[xmtp_common::test(unwrap_try = true)]
576    fn test_serde_wrong_length_fails(#[case] len: usize) {
577        let bad: Vec<u8> = vec![1; len];
578        let bytes = cbor4ii::serde::to_vec(Vec::new(), &bad).unwrap();
579        assert!(cbor4ii::serde::from_slice::<GroupId>(&bytes).is_err());
580    }
581
582    #[xmtp_common::test(unwrap_try = true)]
583    fn test_generate_produces_32_bytes() {
584        use xmtp_common::Generate;
585        let id: GroupId = GroupId::generate();
586        assert_eq!(id.as_slice().len(), 32);
587    }
588
589    #[xmtp_common::test(unwrap_try = true)]
590    fn test_default_is_zero() {
591        let id = GroupId::default();
592        assert_eq!(id.as_slice(), &[0u8; 16][..]);
593    }
594
595    #[xmtp_common::test(unwrap_try = true)]
596    fn test_const_helpers() {
597        assert_eq!(GroupId::ZERO, GroupId::default());
598        assert_eq!(GroupId::ZERO.as_slice(), &[0u8; 16]);
599        assert_eq!(GroupId::ONE.as_slice(), &[1u8; 16]);
600        assert_eq!(GroupId::TWO.as_slice(), &[2u8; 16]);
601        assert_eq!(GroupId::THREE.as_slice(), &[3u8; 16]);
602        assert_eq!(GroupId::FOUR.as_slice(), &[4u8; 16]);
603    }
604
605    #[xmtp_common::test(unwrap_try = true)]
606    fn test_display_debug() {
607        let id = GroupId::from([0x12, 0x34, 0xab, 0xcd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
608        let displayed = format!("{}", id);
609        assert!(displayed.starts_with("1234abcd"));
610        assert_eq!(displayed.len(), 32);
611        assert_eq!(GroupId::from([0xab; 32]).to_string(), "ab".repeat(32));
612        let debugged = format!("{:?}", id);
613        assert!(debugged.starts_with("GroupId("));
614    }
615
616    #[cfg(feature = "diesel")]
617    mod diesel_test {
618        use super::*;
619        use diesel::prelude::*;
620
621        diesel::table! {
622            test_group_ids (id) {
623                id -> Binary,
624            }
625        }
626
627        #[derive(Insertable, Queryable)]
628        #[diesel(table_name = test_group_ids)]
629        struct Row {
630            id: GroupId,
631        }
632
633        fn connection() -> SqliteConnection {
634            let mut conn = SqliteConnection::establish(":memory:").unwrap();
635            diesel::sql_query("CREATE TABLE test_group_ids (id BLOB NOT NULL PRIMARY KEY)")
636                .execute(&mut conn)
637                .unwrap();
638            conn
639        }
640
641        #[rstest]
642        #[case(GroupId::from([0xabu8; 16]))]
643        #[case(GroupId::from([0xabu8; 32]))]
644        #[xmtp_common::test(unwrap_try = true)]
645        fn test_diesel_roundtrip(#[case] id: GroupId) {
646            let mut conn = connection();
647            diesel::insert_into(test_group_ids::table)
648                .values(&Row { id })
649                .execute(&mut conn)
650                .unwrap();
651            let got: Row = test_group_ids::table.first(&mut conn).unwrap();
652            assert_eq!(got.id, id);
653            // The stored blob is the id bytes, with no padding.
654            let stored: Vec<u8> = test_group_ids::table
655                .select(diesel::dsl::sql::<diesel::sql_types::Binary>("id"))
656                .first(&mut conn)
657                .unwrap();
658            assert_eq!(stored, id.to_vec());
659        }
660
661        #[rstest]
662        #[case("X'0102030405060708'")]
663        #[case("zeroblob(33)")]
664        #[xmtp_common::test(unwrap_try = true)]
665        fn test_diesel_wrong_length_errors(#[case] blob: &str) {
666            let mut conn = connection();
667            // Insert a raw blob of the wrong length, bypassing the type.
668            diesel::sql_query(format!("INSERT INTO test_group_ids (id) VALUES ({blob})"))
669                .execute(&mut conn)
670                .unwrap();
671            let result: Result<Row, _> = test_group_ids::table.first(&mut conn);
672            assert!(result.is_err());
673        }
674    }
675}