Skip to main content

conmonrs/
random_id.rs

1//! Random IDs, for the tokens of the streaming server, which a client has to
2//! know to connect, and for names which must not collide.
3
4use std::{fmt, str::FromStr};
5
6/// 128 random bits from the operating system, written as 32 lowercase hex
7/// digits.
8#[derive(Clone, Copy, PartialEq, Eq, Hash)]
9pub(crate) struct RandomId(u128);
10
11impl RandomId {
12    /// The length of the written ID.
13    const LEN: usize = 32;
14
15    /// A new random ID. Panics if the operating system has no randomness,
16    /// which Linux always has.
17    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
30/// The first 8 hex digits of [`fmt::Display`], which tell the IDs in a log
31/// apart without handing them out: the token of a request which got rejected
32/// stays valid.
33impl fmt::Debug for RandomId {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "{:08x}", self.0 >> 96)
36    }
37}
38
39/// Only exactly 32 lowercase hex digits, the way they get written, without
40/// a sign or other characters.
41impl 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/// A string which is no [`RandomId`].
55#[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        // Leading zeros are written as well.
84        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}