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
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub struct FileId(pub [u8; 16]);
71
72impl FileId {
73 #[must_use]
75 pub fn generate() -> FileId {
76 FileId(random())
77 }
78
79 pub fn parse(text: &str) -> Option<FileId> {
82 unhex(text).map(FileId)
83 }
84}
85
86fn random<const N: usize>() -> [u8; N] {
87 let mut bytes = [0; N];
88 aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
91 bytes
92}
93
94fn unhex<const N: usize>(text: &str) -> Option<[u8; N]> {
96 let digits = text.as_bytes();
97 if digits.len() != 2 * N {
98 return None;
99 }
100 let digit = |d: u8| match d {
101 b'0'..=b'9' => Some(d - b'0'),
102 b'a'..=b'f' => Some(d - b'a' + 10),
103 _ => None,
104 };
105 let mut bytes = [0; N];
106 let (pairs, _) = digits.as_chunks::<2>();
107 for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
108 *byte = digit(*high)? << 4 | digit(*low)?;
109 }
110 Some(bytes)
111}
112
113fn hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 bytes.iter().try_for_each(|b| write!(f, "{b:02x}"))
115}
116
117impl fmt::Display for KeyId {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 hex(&self.0, f)
120 }
121}
122
123impl fmt::Display for TenantId {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 hex(&self.0, f)
126 }
127}
128
129impl fmt::Display for VersionId {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 hex(&self.0, f)
132 }
133}
134
135impl fmt::Display for FileId {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 hex(&self.0, f)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn ids_are_written_as_32_lowercase_hex_digits() {
147 let mut bytes = [0; 16];
148 bytes[0] = 0x0a;
149 bytes[15] = 0xff;
150 let expected = format!("0a{}ff", "00".repeat(14));
151 assert_eq!(
152 TenantId(bytes).to_string(),
153 expected,
154 "two lower-case hex digits per byte, in order, zeros kept"
155 );
156 assert_eq!(
157 VersionId(bytes).to_string(),
158 expected,
159 "two lower-case hex digits per byte, in order, zeros kept"
160 );
161 }
162
163 fn varies(ids: &[[u8; 16]]) {
165 for i in 0..16 {
166 assert!(
167 ids.iter().any(|id| id[i] != ids[0][i]),
168 "byte {i} never varies"
169 );
170 }
171 }
172
173 #[test]
174 fn a_key_id_is_64_random_bits_written_as_16_hex_digits() {
175 let ids: Vec<[u8; 8]> = (0..8).map(|_| KeyId::generate().0).collect();
176 for i in 0..8 {
177 assert!(
178 ids.iter().any(|id| id[i] != ids[0][i]),
179 "byte {i} never varies"
180 );
181 }
182 let id = KeyId([0x0a, 0, 0, 0, 0, 0, 0, 0xff]);
183 assert_eq!(
184 id.to_string(),
185 "0a000000000000ff",
186 "two lower-case hex digits per byte, in order, zeros kept"
187 );
188 assert_eq!(
189 KeyId::parse("0a000000000000ff"),
190 Some(id),
191 "parse reads the spelling Display writes"
192 );
193 for bad in [
194 "0A000000000000FF",
195 "0a000000000000f",
196 "0a000000000000ff0",
197 "",
198 ] {
199 assert_eq!(KeyId::parse(bad), None, "{bad:?}");
200 }
201 }
202
203 #[test]
204 fn ids_are_random() {
205 varies(&(0..8).map(|_| VersionId::generate().0).collect::<Vec<_>>());
206 varies(&(0..8).map(|_| TenantId::generate().0).collect::<Vec<_>>());
207 varies(&(0..8).map(|_| FileId::generate().0).collect::<Vec<_>>());
208 }
209
210 #[test]
213 fn a_file_id_parses_only_from_its_own_spelling() {
214 let mut bytes = [0; 16];
215 bytes[0] = 0x0a;
216 bytes[15] = 0xff;
217 assert_eq!(
218 FileId(bytes).to_string(),
219 format!("0a{}ff", "00".repeat(14)),
220 "two lower-case hex digits per byte, in order, zeros kept"
221 );
222 let file = FileId::generate();
223 assert_eq!(
224 FileId::parse(&file.to_string()),
225 Some(file),
226 "parse reads the spelling Display writes"
227 );
228 let text = file.to_string();
229 for bad in misspelled(&text) {
230 if bad != text {
231 assert_eq!(
232 FileId::parse(&bad),
233 None,
234 "not a file id's spelling: {bad:?}"
235 );
236 }
237 }
238 }
239
240 fn misspelled(text: &str) -> [String; 7] {
242 [
243 String::new(),
244 text[..31].to_owned(),
245 format!("{text}0"),
246 text.to_uppercase(),
247 format!("g{}", &text[1..]),
248 format!("{text}.tmp"),
249 format!("+{}", &text[1..]),
250 ]
251 }
252
253 #[test]
254 fn tenant_and_version_ids_parse_only_from_their_own_spelling() {
255 let tenant = TenantId::generate();
256 let version = VersionId::generate();
257 assert_eq!(
258 TenantId::parse(&tenant.to_string()),
259 Some(tenant),
260 "parse reads the spelling Display writes"
261 );
262 assert_eq!(
263 VersionId::parse(&version.to_string()),
264 Some(version),
265 "parse reads the spelling Display writes"
266 );
267 for (text, parsed) in [
268 (
269 tenant.to_string(),
270 (|t: &str| TenantId::parse(t).is_some()) as fn(&str) -> bool,
271 ),
272 (version.to_string(), |t: &str| VersionId::parse(t).is_some()),
273 ] {
274 for bad in misspelled(&text) {
275 if bad != text {
277 assert!(!parsed(&bad), "not an id's own spelling: {bad:?}");
278 }
279 }
280 }
281 }
282}