Skip to main content

pfx_scene/
id.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::sha256::sha256;
7
8pub const ID_LENGTH: usize = 10;
9pub const ID_BITS: u32 = 50;
10
11const ALPHABET: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz";
12const MASK: u64 = (1 << ID_BITS) - 1;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct Id(u64);
16
17impl Id {
18    pub fn from_bits(bits: u64) -> Self {
19        Self(bits & MASK)
20    }
21
22    pub fn derive(key: &[u8]) -> Self {
23        let digest = sha256(key);
24        let mut first = [0; 8];
25        first.copy_from_slice(&digest[..8]);
26        Self(u64::from_be_bytes(first) >> (64 - ID_BITS))
27    }
28
29    pub fn bits(self) -> u64 {
30        self.0
31    }
32
33    pub fn parse(text: &str) -> Result<Self, IdError> {
34        if text.chars().count() != ID_LENGTH {
35            return Err(IdError::Length(text.chars().count()));
36        }
37        let mut bits = 0u64;
38        for character in text.chars() {
39            let Some(value) = ALPHABET
40                .iter()
41                .position(|&letter| char::from(letter) == character)
42            else {
43                return Err(IdError::Character(character));
44            };
45            bits = (bits << 5) | value as u64;
46        }
47        Ok(Self(bits))
48    }
49}
50
51impl fmt::Display for Id {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        let mut text = [0u8; ID_LENGTH];
54        for (place, slot) in text.iter_mut().enumerate() {
55            let shift = 5 * (ID_LENGTH - 1 - place);
56            *slot = ALPHABET[((self.0 >> shift) & 31) as usize];
57        }
58        f.write_str(std::str::from_utf8(&text).unwrap_or_default())
59    }
60}
61
62impl FromStr for Id {
63    type Err = IdError;
64
65    fn from_str(text: &str) -> Result<Self, Self::Err> {
66        Self::parse(text)
67    }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum IdError {
72    Length(usize),
73    Character(char),
74}
75
76impl fmt::Display for IdError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::Length(count) => write!(
80                f,
81                "an id is {ID_LENGTH} characters, not {count}: lowercase Crockford base32"
82            ),
83            Self::Character(character) => write!(
84                f,
85                "an id takes 0-9 and a-z without i, l, o and u, not {character:?}"
86            ),
87        }
88    }
89}
90
91impl std::error::Error for IdError {}
92
93impl Serialize for Id {
94    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95        serializer.collect_str(self)
96    }
97}
98
99impl<'de> Deserialize<'de> for Id {
100    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101        let text = String::deserialize(deserializer)?;
102        Self::parse(&text).map_err(serde::de::Error::custom)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn an_id_is_ten_crockford_characters_and_round_trips() {
112        let id = Id::parse("7k2m9q4xzr").unwrap();
113        assert_eq!(id.to_string(), "7k2m9q4xzr");
114        assert_eq!(Id::from_bits(id.bits()), id);
115        assert_eq!(Id::from_bits(0).to_string(), "0000000000");
116        assert_eq!(Id::from_bits(u64::MAX).to_string(), "zzzzzzzzzz");
117        assert_eq!(Id::from_bits(u64::MAX).bits(), (1 << 50) - 1);
118        assert_eq!(Id::from_bits(31).to_string(), "000000000z");
119    }
120
121    #[test]
122    fn a_bad_id_is_refused() {
123        assert_eq!(Id::parse("7k2m9q4xz"), Err(IdError::Length(9)));
124        assert_eq!(Id::parse("7k2m9q4xzrr"), Err(IdError::Length(11)));
125        assert_eq!(Id::parse("7k2m9q4xzi"), Err(IdError::Character('i')));
126        assert_eq!(Id::parse("7K2M9Q4XZR"), Err(IdError::Character('K')));
127        assert!(Id::parse("7k2m9q4xzu").is_err());
128        assert!(Id::parse("7k2m9q4xzo").is_err());
129        assert!(Id::parse("7k2m9q4xzl").is_err());
130    }
131
132    #[test]
133    fn a_derived_id_is_the_first_fifty_bits_of_the_sha256() {
134        let digest = sha256(b"abc");
135        let expected = u64::from_be_bytes(digest[..8].try_into().unwrap()) >> 14;
136        assert_eq!(Id::derive(b"abc").bits(), expected);
137        assert_eq!(Id::derive(b"abc").to_string(), "q9w1dfwf07");
138        assert_eq!(Id::derive(b"abc"), Id::derive(b"abc"));
139        assert_ne!(Id::derive(b"abc"), Id::derive(b"abd"));
140    }
141}