ferroid/
base32.rs

1use crate::{
2    Error, Result, Snowflake, SnowflakeDiscordId, SnowflakeInstagramId, SnowflakeMastodonId,
3    SnowflakeTwitterId,
4};
5use base32::{decode, encode, Alphabet};
6use std::convert::TryInto;
7
8const U32_SIZE: usize = std::mem::size_of::<u32>();
9const U64_SIZE: usize = std::mem::size_of::<u64>();
10const U128_SIZE: usize = std::mem::size_of::<u128>();
11
12/// A trait for types that can be encoded to and decoded from big-endian bytes.
13pub trait BeBytes: Sized {
14    type ByteArray: AsRef<[u8]>;
15
16    fn to_be_bytes(self) -> Self::ByteArray;
17
18    fn from_be_bytes(bytes: &[u8]) -> Option<Self>;
19}
20
21impl BeBytes for u32 {
22    type ByteArray = [u8; U32_SIZE];
23
24    fn to_be_bytes(self) -> Self::ByteArray {
25        self.to_be_bytes()
26    }
27
28    fn from_be_bytes(bytes: &[u8]) -> Option<Self> {
29        let arr: [u8; U32_SIZE] = bytes.try_into().ok()?;
30        Some(Self::from_be_bytes(arr))
31    }
32}
33
34impl BeBytes for u64 {
35    type ByteArray = [u8; U64_SIZE];
36
37    fn to_be_bytes(self) -> Self::ByteArray {
38        self.to_be_bytes()
39    }
40
41    fn from_be_bytes(bytes: &[u8]) -> Option<Self> {
42        let arr: [u8; U64_SIZE] = bytes.try_into().ok()?;
43        Some(Self::from_be_bytes(arr))
44    }
45}
46
47impl BeBytes for u128 {
48    type ByteArray = [u8; U128_SIZE];
49
50    fn to_be_bytes(self) -> Self::ByteArray {
51        self.to_be_bytes()
52    }
53
54    fn from_be_bytes(bytes: &[u8]) -> Option<Self> {
55        let arr: [u8; U128_SIZE] = bytes.try_into().ok()?;
56        Some(Self::from_be_bytes(arr))
57    }
58}
59
60/// A trait for types that can be encoded to and decoded from base32 (crockford) strings.
61pub trait Base32: Snowflake + Sized
62where
63    Self::Ty: BeBytes,
64{
65    fn encode(&self) -> String {
66        let bytes = self.to_raw().to_be_bytes();
67        encode(Alphabet::Crockford, bytes.as_ref())
68    }
69
70    fn decode(s: &str) -> Result<Self> {
71        let bytes = decode(Alphabet::Crockford, s).ok_or(Error::DecodeNonAsciiValue)?;
72        let raw = Self::Ty::from_be_bytes(&bytes).ok_or(Error::DecodeInvalidLen)?;
73        Ok(Self::from_raw(raw))
74    }
75}
76
77impl Base32 for SnowflakeTwitterId {}
78impl Base32 for SnowflakeDiscordId {}
79impl Base32 for SnowflakeInstagramId {}
80impl Base32 for SnowflakeMastodonId {}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use core::any::type_name;
86    use core::fmt;
87
88    fn test_encode_decode<T>(id: T, label: &str)
89    where
90        T: Snowflake + Base32 + PartialEq + fmt::Debug,
91        T::Ty: BeBytes + fmt::Binary + fmt::LowerHex + fmt::Display + fmt::Debug,
92    {
93        let raw = id.to_raw();
94        let encoded = id.encode();
95        let decoded = T::decode(&encoded).expect("decode should succeed");
96
97        let type_name = type_name::<T>();
98
99        println!("=== {} {} ===", type_name, label);
100        println!("id (raw decimal): {}", raw);
101        println!("id (raw binary):  {:064b}", raw);
102        println!("timestamp:  0x{:x}", id.timestamp());
103        println!("machine id: 0x{:x}", id.machine_id());
104        println!("sequence:   0x{:x}", id.sequence());
105        println!("encoded:    {}", encoded);
106        println!("decoded:    {:?}", decoded);
107
108        assert_eq!(id, decoded, "{} roundtrip failed for {}", label, type_name);
109    }
110
111    #[test]
112    fn twitter_max() {
113        let id = SnowflakeTwitterId::from_components(
114            SnowflakeTwitterId::max_timestamp(),
115            SnowflakeTwitterId::max_machine_id(),
116            SnowflakeTwitterId::max_sequence(),
117        );
118        test_encode_decode(id, "max");
119        assert_eq!(id.to_raw(), 9_223_372_036_854_775_807) // 1 bit reserved
120    }
121
122    #[test]
123    fn twitter_zero() {
124        let id = SnowflakeTwitterId::from_components(
125            SnowflakeTwitterId::ZERO,
126            SnowflakeTwitterId::ZERO,
127            SnowflakeTwitterId::ZERO,
128        );
129        test_encode_decode(id, "zero");
130        assert_eq!(id.to_raw(), 0)
131    }
132
133    #[test]
134    fn discord_max() {
135        let id = SnowflakeDiscordId::from_components(
136            SnowflakeDiscordId::max_timestamp(),
137            SnowflakeDiscordId::max_machine_id(),
138            SnowflakeDiscordId::max_sequence(),
139        );
140        test_encode_decode(id, "max");
141        assert_eq!(id.to_raw(), 18_446_744_073_709_551_615)
142    }
143
144    #[test]
145    fn discord_zero() {
146        let id = SnowflakeDiscordId::from_components(
147            SnowflakeDiscordId::ZERO,
148            SnowflakeDiscordId::ZERO,
149            SnowflakeDiscordId::ZERO,
150        );
151        test_encode_decode(id, "zero");
152        assert_eq!(id.to_raw(), 0)
153    }
154
155    #[test]
156    fn instagram_max() {
157        let id = SnowflakeInstagramId::from_components(
158            SnowflakeInstagramId::max_timestamp(),
159            SnowflakeInstagramId::max_machine_id(),
160            SnowflakeInstagramId::max_sequence(),
161        );
162        test_encode_decode(id, "max");
163        assert_eq!(id.to_raw(), 18_446_744_073_709_551_615)
164    }
165
166    #[test]
167    fn instagram_zero() {
168        let id = SnowflakeInstagramId::from_components(
169            SnowflakeInstagramId::ZERO,
170            SnowflakeInstagramId::ZERO,
171            SnowflakeInstagramId::ZERO,
172        );
173        test_encode_decode(id, "zero");
174        assert_eq!(id.to_raw(), 0)
175    }
176
177    #[test]
178    fn mastodon_max() {
179        let id = SnowflakeMastodonId::from_components(
180            SnowflakeMastodonId::max_timestamp(),
181            SnowflakeMastodonId::max_machine_id(),
182            SnowflakeMastodonId::max_sequence(),
183        );
184        test_encode_decode(id, "max");
185        assert_eq!(id.to_raw(), 18_446_744_073_709_551_615)
186    }
187
188    #[test]
189    fn mastodon_zero() {
190        let id = SnowflakeMastodonId::from_components(
191            SnowflakeMastodonId::ZERO,
192            SnowflakeMastodonId::ZERO,
193            SnowflakeMastodonId::ZERO,
194        );
195        test_encode_decode(id, "zero");
196        assert_eq!(id.to_raw(), 0)
197    }
198
199    #[test]
200    fn decode_invalid_character_fails() {
201        // Base32 Crockford disallows symbols like `@`
202        let invalid = "01234@6789ABCDEF";
203        let result = SnowflakeTwitterId::decode(invalid);
204        assert!(matches!(result, Err(Error::DecodeNonAsciiValue)));
205    }
206
207    #[test]
208    fn decode_invalid_length_fails() {
209        // Shorter than 13-byte base32 for u64 (decoded slice won't be 8 bytes)
210        let too_short = "ABC";
211        let result = SnowflakeTwitterId::decode(too_short);
212        assert!(matches!(result, Err(Error::DecodeInvalidLen)));
213    }
214}