1use std::{fmt, str::FromStr};
5
6#[derive(Clone, Copy, PartialEq, Eq, Hash)]
9pub(crate) struct RandomId(u128);
10
11impl RandomId {
12 const LEN: usize = 32;
14
15 pub(crate) fn new() -> Self {
18 let mut bytes = [0; 16];
19 getrandom::fill(&mut bytes).expect("random bytes of the operating system");
20 Self(u128::from_ne_bytes(bytes))
21 }
22}
23
24impl fmt::Display for RandomId {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 write!(f, "{:032x}", self.0)
27 }
28}
29
30impl fmt::Debug for RandomId {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(f, "{:08x}", self.0 >> 96)
36 }
37}
38
39impl FromStr for RandomId {
42 type Err = InvalidRandomId;
43
44 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 if s.len() != Self::LEN || !s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
46 return Err(InvalidRandomId);
47 }
48 u128::from_str_radix(s, 16)
49 .map(Self)
50 .map_err(|_| InvalidRandomId)
51 }
52}
53
54#[derive(Debug, PartialEq, Eq)]
56pub(crate) struct InvalidRandomId;
57
58impl fmt::Display for InvalidRandomId {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "expected {} lowercase hex digits", RandomId::LEN)
61 }
62}
63
64impl std::error::Error for InvalidRandomId {}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn ids_round_trip() {
72 let id = RandomId::new();
73 let written = id.to_string();
74 assert_eq!(written.len(), RandomId::LEN);
75 assert!(
76 written
77 .bytes()
78 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
79 );
80 assert_eq!(written.parse(), Ok(id));
81 assert_ne!(RandomId::new(), id);
82
83 assert_eq!(RandomId(1).to_string(), "00000000000000000000000000000001");
85 assert_eq!(format!("{id:?}"), written[..8]);
86 assert_eq!(format!("{:?}", RandomId(u128::MAX >> 4)), "0fffffff");
87 }
88
89 #[test]
90 fn only_hex_digits_parse() {
91 let valid = "0123456789abcdef0123456789abcdef";
92 assert_eq!(valid.parse::<RandomId>().map(|_| ()), Ok(()));
93 for invalid in [
94 "",
95 &valid[1..],
96 &format!("{valid}0"),
97 "+123456789abcdef0123456789abcdef",
98 "0123456789abcdef0123456789abcdeg",
99 "0123456789abcdef0123456789abcdeF",
100 "01234567-89ab-cdef-0123-456789abcdef",
101 ] {
102 assert_eq!(
103 invalid.parse::<RandomId>(),
104 Err(InvalidRandomId),
105 "{invalid}"
106 );
107 }
108 }
109}