Skip to main content

vcon_server/
ids.rs

1//! Identifiers, written in lowercase hex: a tenant or a version is 128
2//! random bits, a key 64 bits.
3
4use std::fmt;
5
6/// Names a key without revealing it: an encryption key by the start of
7/// its SHA-256 (`Key::id`), an access key by 64 random bits.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct KeyId(pub [u8; 8]);
10
11impl KeyId {
12    /// A new random key id.
13    #[must_use]
14    pub fn generate() -> KeyId {
15        KeyId(random())
16    }
17
18    /// Reads a key id written by `Display`: exactly 16 lowercase hex
19    /// digits.
20    pub fn parse(text: &str) -> Option<KeyId> {
21        unhex(text).map(KeyId)
22    }
23
24    /// The id's 8 bytes, as catalogs store them.
25    pub(crate) fn bytes(self) -> [u8; 8] {
26        self.0
27    }
28}
29
30/// A tenant.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct TenantId(pub [u8; 16]);
33
34impl TenantId {
35    /// A new random tenant id.
36    #[must_use]
37    pub fn generate() -> TenantId {
38        TenantId(random())
39    }
40
41    /// Reads a tenant id written by `Display`: exactly 32 lowercase hex
42    /// digits.
43    pub fn parse(text: &str) -> Option<TenantId> {
44        unhex(text).map(TenantId)
45    }
46}
47
48/// One stored version of a vCon.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct VersionId(pub [u8; 16]);
51
52impl VersionId {
53    /// A new random version id.
54    #[must_use]
55    pub fn generate() -> VersionId {
56        VersionId(random())
57    }
58}
59
60impl VersionId {
61    /// Reads a version id written by `Display`: exactly 32 lowercase hex
62    /// digits.
63    pub fn parse(text: &str) -> Option<VersionId> {
64        unhex(text).map(VersionId)
65    }
66}
67
68/// A Parquet file of packed versions.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub struct FileId(pub [u8; 16]);
71
72impl FileId {
73    /// A new random file id.
74    #[must_use]
75    pub fn generate() -> FileId {
76        FileId(random())
77    }
78
79    /// Reads a file id written by `Display`: exactly 32 lowercase hex
80    /// digits.
81    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's RAND_bytes aborts the process when it cannot get enough
89    // randomness, so `fill` never returns its error.
90    aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
91    bytes
92}
93
94/// Reads exactly 2N lowercase hex digits.
95fn 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    /// Every byte of `ids` varies across them.
164    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    /// A file id is written and read as a version id is, and only from its
211    /// own spelling.
212    #[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    /// The spellings parse refuses, given one id's own spelling `text`.
241    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                // An id with no letter is its own upper-case spelling.
276                if bad != text {
277                    assert!(!parsed(&bad), "not an id's own spelling: {bad:?}");
278                }
279            }
280        }
281    }
282}