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}