crockford_uuid/
lib.rs

1pub mod crock_ford {
2
3    use bytes::BytesMut;
4    use lazy_static::lazy_static;
5    use num_bigint::{BigUint, ToBigUint};
6    use ring::rand::{SecureRandom, SystemRandom};
7
8    const BYTE_SIZE: usize = 15;
9    const CROCKFORD_CHECKSUM_CHARS: &str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U";
10    // a prime number greater than 32 for checksum derivation
11    const CROCKFORD_MODULO_PRIME: usize = 37;
12
13    fn rng() -> &'static dyn SecureRandom {
14        use std::ops::Deref;
15
16        lazy_static! {
17            static ref RANDOM: SystemRandom = SystemRandom::new();
18        }
19
20        RANDOM.deref()
21    }
22
23    #[derive(Debug)]
24    struct Bytes(BytesMut);
25
26    impl Bytes {
27        pub fn to_slice(&self) -> &[u8] {
28            &self.0[..]
29        }
30
31        pub fn to_int(&self) -> BigUint {
32            BigUint::from_bytes_be(&self.0[..])
33        }
34
35        pub fn to_vec(&self) -> Vec<u8> {
36            self.0.to_vec()
37        }
38
39        pub fn derive_crockford_checksum(&self) -> BigUint {
40            self.to_int() % ToBigUint::to_biguint(&CROCKFORD_MODULO_PRIME).unwrap()
41        }
42
43        pub fn new(size: usize) -> Result<Self, String> {
44            let mut bytes = vec![0; size];
45            rng().fill(&mut bytes).map_err(|e| e.to_string())?;
46            Ok(Self(BytesMut::from_iter(bytes.iter())))
47        }
48    }
49
50    impl TryFrom<BigUint> for Bytes {
51        type Error = &'static str;
52        fn try_from(value: BigUint) -> Result<Self, Self::Error> {
53            let bytes = value.to_bytes_be();
54            Bytes::try_from(bytes)
55        }
56    }
57
58    impl TryFrom<Vec<u8>> for Bytes {
59        type Error = &'static str;
60        fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
61            let bytes = BytesMut::try_from(&value[..])
62                .map_err(|_| "unable to convert value to mutable byte")?;
63            Ok(Self(bytes))
64        }
65    }
66
67    #[derive(Debug)]
68    pub struct Uuid {
69        bytes: Bytes,
70        checksum: BigUint,
71    }
72
73    impl Uuid {
74        pub fn new() -> Self {
75            let bytes = Bytes::new(BYTE_SIZE).expect("failed to generate random bytes");
76            let checksum = bytes.derive_crockford_checksum();
77            Self { bytes, checksum }
78        }
79
80        pub fn value(&self) -> String {
81            base32::encode(base32::Alphabet::Crockford, &self.bytes.to_slice())
82        }
83
84        fn get_checksum_char(checksum: &BigUint) -> char {
85            let checksum: i8 = checksum.try_into().unwrap();
86            CROCKFORD_CHECKSUM_CHARS
87                .chars()
88                .nth(checksum.abs() as usize)
89                .unwrap()
90        }
91
92        fn value_with_checksum(&self) -> String {
93            format!(
94                "{}{}",
95                self.value(),
96                Uuid::get_checksum_char(&self.checksum)
97            )
98        }
99
100        fn len() -> usize {
101            // we are trying to fit 8 bits bytes into a 5 bit char
102            (BYTE_SIZE * 8 / 5) + 1
103        }
104
105        fn from_str(value: &str) -> Result<Self, &'static str> {
106            if value.len() != Uuid::len() {
107                return Err("invalid string length");
108            }
109
110            let value = value.to_ascii_uppercase();
111
112            let id = &value[..=(Uuid::len() - 2)];
113            let bytes = match base32::decode(base32::Alphabet::Crockford, id) {
114                None => return Err("invalid uuid str"),
115                Some(d) => Bytes::try_from(d)?,
116            };
117
118            let checksum = bytes.derive_crockford_checksum();
119            if Uuid::get_checksum_char(&checksum)
120                == value[(Uuid::len() - 1)..].chars().nth(0).unwrap()
121            {
122                Ok(Self { bytes, checksum })
123            } else {
124                Err("invalid uuid str")
125            }
126        }
127    }
128
129    impl std::fmt::Display for Uuid {
130        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
131            write!(f, "{}", self.value_with_checksum())
132        }
133    }
134
135    impl TryFrom<&str> for Uuid {
136        type Error = &'static str;
137        fn try_from(value: &str) -> Result<Self, Self::Error> {
138            Uuid::from_str(value)
139        }
140    }
141
142    impl TryFrom<String> for Uuid {
143        type Error = &'static str;
144        fn try_from(value: String) -> Result<Self, Self::Error> {
145            Uuid::from_str(value.as_str())
146        }
147    }
148
149    impl TryFrom<BigUint> for Uuid {
150        type Error = &'static str;
151        fn try_from(value: BigUint) -> Result<Self, Self::Error> {
152            let bytes: Bytes = value
153                .try_into()
154                .map_err(|_| "unable to convert bigint to uuid")?;
155            let checksum = bytes.derive_crockford_checksum();
156            Ok(Self { bytes, checksum })
157        }
158    }
159
160    impl Into<BigUint> for Uuid {
161        fn into(self) -> BigUint {
162            self.bytes.to_int()
163        }
164    }
165
166    impl Into<Vec<u8>> for Uuid {
167        fn into(self) -> Vec<u8> {
168            self.bytes.to_vec()
169        }
170    }
171
172    impl Into<Bytes> for Uuid {
173        fn into(self) -> Bytes {
174            self.bytes
175        }
176    }
177
178    impl PartialEq<Uuid> for Uuid {
179        fn eq(&self, other: &Uuid) -> bool {
180            self.value_with_checksum() == other.value_with_checksum()
181        }
182    }
183
184    impl PartialEq<String> for Uuid {
185        fn eq(&self, other: &String) -> bool {
186            match Uuid::from_str(other) {
187                Ok(uuid) => *self == uuid,
188                Err(_) => false,
189            }
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use crate::crock_ford::Uuid;
197    use num_bigint::BigUint;
198
199    fn str_uuid() -> &'static str {
200        "4s0y2vz7sf4vghnznytz9gvq6"
201    }
202
203    #[test]
204    fn generate() {
205        let uuid = Uuid::new();
206        println!("uuid={}", uuid);
207        assert_eq!(uuid.to_string().len(), 25); // 24 char identifier, 1 char checksum
208    }
209
210    #[test]
211    fn generate_from_string() {
212        let result: Uuid = str_uuid().try_into().unwrap();
213        assert_eq!(result.to_string().to_lowercase(), str_uuid());
214    }
215
216    #[test]
217    fn compare_two_uuid_of_same_value() {
218        let first: Uuid = str_uuid().try_into().unwrap();
219        let second: Uuid = str_uuid().try_into().unwrap();
220        assert_eq!(first, second);
221    }
222
223    #[test]
224    fn compare_two_uuid_of_diff_value() {
225        let first: Uuid = str_uuid().try_into().unwrap();
226        let second = Uuid::new();
227        assert_ne!(first, second);
228    }
229
230    #[test]
231    fn compare_uuid_with_string() {
232        let uuid: Uuid = str_uuid().try_into().unwrap();
233        assert_eq!(uuid, str_uuid().to_string());
234    }
235
236    #[test]
237    fn get_uuid_as_integer_value() {
238        let uuid: Uuid = str_uuid().try_into().unwrap();
239        let int_value: BigUint = uuid.into();
240        println!("{}", int_value);
241    }
242
243    // get as byte
244    #[test]
245    fn get_uuid_as_byte_value() {
246        let uuid: Vec<u8> = Uuid::new().into();
247        println!("{:?}", uuid);
248    }
249
250    // compare with int and byte
251    #[test]
252    fn convert_integer_to_uuid() {
253        let int_value: BigUint = Uuid::try_from(str_uuid()).unwrap().try_into().unwrap();
254        let uuid: Uuid = int_value.try_into().unwrap();
255        assert_eq!(uuid, str_uuid().to_string())
256    }
257}