1use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct KeyId(pub [u8; 8]);
10
11impl KeyId {
12 #[must_use]
14 pub fn generate() -> KeyId {
15 KeyId(random())
16 }
17
18 pub fn parse(text: &str) -> Option<KeyId> {
21 unhex(text).map(KeyId)
22 }
23
24 pub(crate) fn bytes(self) -> [u8; 8] {
26 self.0
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct TenantId(pub [u8; 16]);
33
34impl TenantId {
35 #[must_use]
37 pub fn generate() -> TenantId {
38 TenantId(random())
39 }
40
41 pub fn parse(text: &str) -> Option<TenantId> {
44 unhex(text).map(TenantId)
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct VersionId(pub [u8; 16]);
51
52impl VersionId {
53 #[must_use]
55 pub fn generate() -> VersionId {
56 VersionId(random())
57 }
58}
59
60impl VersionId {
61 pub fn parse(text: &str) -> Option<VersionId> {
64 unhex(text).map(VersionId)
65 }
66}
67
68fn random<const N: usize>() -> [u8; N] {
69 let mut bytes = [0; N];
70 aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
73 bytes
74}
75
76fn unhex<const N: usize>(text: &str) -> Option<[u8; N]> {
78 let digits = text.as_bytes();
79 if digits.len() != 2 * N {
80 return None;
81 }
82 let digit = |d: u8| match d {
83 b'0'..=b'9' => Some(d - b'0'),
84 b'a'..=b'f' => Some(d - b'a' + 10),
85 _ => None,
86 };
87 let mut bytes = [0; N];
88 let (pairs, _) = digits.as_chunks::<2>();
89 for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
90 *byte = digit(*high)? << 4 | digit(*low)?;
91 }
92 Some(bytes)
93}
94
95fn hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 bytes.iter().try_for_each(|b| write!(f, "{b:02x}"))
97}
98
99impl fmt::Display for KeyId {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 hex(&self.0, f)
102 }
103}
104
105impl fmt::Display for TenantId {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 hex(&self.0, f)
108 }
109}
110
111impl fmt::Display for VersionId {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 hex(&self.0, f)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn ids_are_written_as_32_lowercase_hex_digits() {
123 let mut bytes = [0; 16];
124 bytes[0] = 0x0a;
125 bytes[15] = 0xff;
126 let expected = format!("0a{}ff", "00".repeat(14));
127 assert_eq!(
128 TenantId(bytes).to_string(),
129 expected,
130 "two lower-case hex digits per byte, in order, zeros kept"
131 );
132 assert_eq!(
133 VersionId(bytes).to_string(),
134 expected,
135 "two lower-case hex digits per byte, in order, zeros kept"
136 );
137 }
138
139 fn varies(ids: &[[u8; 16]]) {
141 for i in 0..16 {
142 assert!(
143 ids.iter().any(|id| id[i] != ids[0][i]),
144 "byte {i} never varies"
145 );
146 }
147 }
148
149 #[test]
150 fn a_key_id_is_64_random_bits_written_as_16_hex_digits() {
151 let ids: Vec<[u8; 8]> = (0..8).map(|_| KeyId::generate().0).collect();
152 for i in 0..8 {
153 assert!(
154 ids.iter().any(|id| id[i] != ids[0][i]),
155 "byte {i} never varies"
156 );
157 }
158 let id = KeyId([0x0a, 0, 0, 0, 0, 0, 0, 0xff]);
159 assert_eq!(
160 id.to_string(),
161 "0a000000000000ff",
162 "two lower-case hex digits per byte, in order, zeros kept"
163 );
164 assert_eq!(
165 KeyId::parse("0a000000000000ff"),
166 Some(id),
167 "parse reads the spelling Display writes"
168 );
169 for bad in [
170 "0A000000000000FF",
171 "0a000000000000f",
172 "0a000000000000ff0",
173 "",
174 ] {
175 assert_eq!(KeyId::parse(bad), None, "{bad:?}");
176 }
177 }
178
179 #[test]
180 fn ids_are_random() {
181 varies(&(0..8).map(|_| VersionId::generate().0).collect::<Vec<_>>());
182 varies(&(0..8).map(|_| TenantId::generate().0).collect::<Vec<_>>());
183 }
184
185 fn misspelled(text: &str) -> [String; 7] {
187 [
188 String::new(),
189 text[..31].to_owned(),
190 format!("{text}0"),
191 text.to_uppercase(),
192 format!("g{}", &text[1..]),
193 format!("{text}.tmp"),
194 format!("+{}", &text[1..]),
195 ]
196 }
197
198 #[test]
199 fn tenant_and_version_ids_parse_only_from_their_own_spelling() {
200 let tenant = TenantId::generate();
201 let version = VersionId::generate();
202 assert_eq!(
203 TenantId::parse(&tenant.to_string()),
204 Some(tenant),
205 "parse reads the spelling Display writes"
206 );
207 assert_eq!(
208 VersionId::parse(&version.to_string()),
209 Some(version),
210 "parse reads the spelling Display writes"
211 );
212 for (text, parsed) in [
213 (
214 tenant.to_string(),
215 (|t: &str| TenantId::parse(t).is_some()) as fn(&str) -> bool,
216 ),
217 (version.to_string(), |t: &str| VersionId::parse(t).is_some()),
218 ] {
219 for bad in misspelled(&text) {
220 if bad != text {
222 assert!(!parsed(&bad), "not an id's own spelling: {bad:?}");
223 }
224 }
225 }
226 }
227}