1use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct KeyId(pub [u8; 8]);
10
11impl KeyId {
12 pub fn generate() -> KeyId {
14 KeyId(random())
15 }
16
17 pub fn parse(text: &str) -> Option<KeyId> {
20 unhex(text).map(KeyId)
21 }
22
23 pub(crate) fn bytes(self) -> [u8; 8] {
25 self.0
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct TenantId(pub [u8; 16]);
32
33impl TenantId {
34 pub fn generate() -> TenantId {
36 TenantId(random())
37 }
38
39 pub fn parse(text: &str) -> Option<TenantId> {
42 unhex(text).map(TenantId)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub struct VersionId(pub [u8; 16]);
49
50impl VersionId {
51 pub fn generate() -> VersionId {
53 VersionId(random())
54 }
55}
56
57impl VersionId {
58 pub fn parse(text: &str) -> Option<VersionId> {
61 unhex(text).map(VersionId)
62 }
63}
64
65fn random<const N: usize>() -> [u8; N] {
66 let mut bytes = [0; N];
67 aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
70 bytes
71}
72
73fn unhex<const N: usize>(text: &str) -> Option<[u8; N]> {
75 let digits = text.as_bytes();
76 if digits.len() != 2 * N {
77 return None;
78 }
79 let digit = |d: u8| match d {
80 b'0'..=b'9' => Some(d - b'0'),
81 b'a'..=b'f' => Some(d - b'a' + 10),
82 _ => None,
83 };
84 let mut bytes = [0; N];
85 let (pairs, _) = digits.as_chunks::<2>();
86 for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
87 *byte = digit(*high)? << 4 | digit(*low)?;
88 }
89 Some(bytes)
90}
91
92fn hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 bytes.iter().try_for_each(|b| write!(f, "{b:02x}"))
94}
95
96impl fmt::Display for KeyId {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 hex(&self.0, f)
99 }
100}
101
102impl fmt::Display for TenantId {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 hex(&self.0, f)
105 }
106}
107
108impl fmt::Display for VersionId {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 hex(&self.0, f)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn ids_are_written_as_32_lowercase_hex_digits() {
120 let mut bytes = [0; 16];
121 bytes[0] = 0x0a;
122 bytes[15] = 0xff;
123 let expected = format!("0a{}ff", "00".repeat(14));
124 assert_eq!(TenantId(bytes).to_string(), expected);
125 assert_eq!(VersionId(bytes).to_string(), expected);
126 }
127
128 fn varies(ids: &[[u8; 16]]) {
130 for i in 0..16 {
131 assert!(
132 ids.iter().any(|id| id[i] != ids[0][i]),
133 "byte {i} never varies"
134 );
135 }
136 }
137
138 #[test]
139 fn a_key_id_is_64_random_bits_written_as_16_hex_digits() {
140 let ids: Vec<[u8; 8]> = (0..8).map(|_| KeyId::generate().0).collect();
141 for i in 0..8 {
142 assert!(
143 ids.iter().any(|id| id[i] != ids[0][i]),
144 "byte {i} never varies"
145 );
146 }
147 let id = KeyId([0x0a, 0, 0, 0, 0, 0, 0, 0xff]);
148 assert_eq!(id.to_string(), "0a000000000000ff");
149 assert_eq!(KeyId::parse("0a000000000000ff"), Some(id));
150 for bad in [
151 "0A000000000000FF",
152 "0a000000000000f",
153 "0a000000000000ff0",
154 "",
155 ] {
156 assert_eq!(KeyId::parse(bad), None, "{bad:?}");
157 }
158 }
159
160 #[test]
161 fn ids_are_random() {
162 varies(&(0..8).map(|_| VersionId::generate().0).collect::<Vec<_>>());
163 varies(&(0..8).map(|_| TenantId::generate().0).collect::<Vec<_>>());
164 }
165
166 #[test]
167 fn a_tenant_id_parses_only_from_its_own_spelling() {
168 let id = TenantId::generate();
169 assert_eq!(TenantId::parse(&id.to_string()), Some(id));
170 let text = id.to_string();
171 for bad in [
172 text.to_uppercase(),
173 text[..31].to_owned(),
174 format!("{text}0"),
175 ] {
176 if bad != text {
177 assert_eq!(TenantId::parse(&bad), None, "{bad:?}");
178 }
179 }
180 }
181
182 #[test]
183 fn a_version_id_parses_only_from_its_own_spelling() {
184 let id = VersionId::generate();
185 assert_eq!(VersionId::parse(&id.to_string()), Some(id));
186 let text = id.to_string();
187 let bad = [
188 String::new(),
189 text[..31].to_owned(),
190 format!("{text}0"),
191 text.to_uppercase(),
192 format!("g{}", &text[1..]),
193 format!("{}.tmp", text),
194 format!("+{}", &text[1..]),
195 ];
196 for bad in bad {
197 if bad != text {
198 assert_eq!(VersionId::parse(&bad), None, "{bad:?}");
199 }
200 }
201 }
202}