use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyId(pub [u8; 8]);
impl KeyId {
#[must_use]
pub fn generate() -> KeyId {
KeyId(random())
}
pub fn parse(text: &str) -> Option<KeyId> {
unhex(text).map(KeyId)
}
pub(crate) fn bytes(self) -> [u8; 8] {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TenantId(pub [u8; 16]);
impl TenantId {
#[must_use]
pub fn generate() -> TenantId {
TenantId(random())
}
pub fn parse(text: &str) -> Option<TenantId> {
unhex(text).map(TenantId)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VersionId(pub [u8; 16]);
impl VersionId {
#[must_use]
pub fn generate() -> VersionId {
VersionId(random())
}
}
impl VersionId {
pub fn parse(text: &str) -> Option<VersionId> {
unhex(text).map(VersionId)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId(pub [u8; 16]);
impl FileId {
#[must_use]
pub fn generate() -> FileId {
FileId(random())
}
pub fn parse(text: &str) -> Option<FileId> {
unhex(text).map(FileId)
}
}
fn random<const N: usize>() -> [u8; N] {
let mut bytes = [0; N];
aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
bytes
}
fn unhex<const N: usize>(text: &str) -> Option<[u8; N]> {
let digits = text.as_bytes();
if digits.len() != 2 * N {
return None;
}
let digit = |d: u8| match d {
b'0'..=b'9' => Some(d - b'0'),
b'a'..=b'f' => Some(d - b'a' + 10),
_ => None,
};
let mut bytes = [0; N];
let (pairs, _) = digits.as_chunks::<2>();
for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
*byte = digit(*high)? << 4 | digit(*low)?;
}
Some(bytes)
}
fn hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
bytes.iter().try_for_each(|b| write!(f, "{b:02x}"))
}
impl fmt::Display for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex(&self.0, f)
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex(&self.0, f)
}
}
impl fmt::Display for VersionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex(&self.0, f)
}
}
impl fmt::Display for FileId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex(&self.0, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_are_written_as_32_lowercase_hex_digits() {
let mut bytes = [0; 16];
bytes[0] = 0x0a;
bytes[15] = 0xff;
let expected = format!("0a{}ff", "00".repeat(14));
assert_eq!(
TenantId(bytes).to_string(),
expected,
"two lower-case hex digits per byte, in order, zeros kept"
);
assert_eq!(
VersionId(bytes).to_string(),
expected,
"two lower-case hex digits per byte, in order, zeros kept"
);
}
fn varies(ids: &[[u8; 16]]) {
for i in 0..16 {
assert!(
ids.iter().any(|id| id[i] != ids[0][i]),
"byte {i} never varies"
);
}
}
#[test]
fn a_key_id_is_64_random_bits_written_as_16_hex_digits() {
let ids: Vec<[u8; 8]> = (0..8).map(|_| KeyId::generate().0).collect();
for i in 0..8 {
assert!(
ids.iter().any(|id| id[i] != ids[0][i]),
"byte {i} never varies"
);
}
let id = KeyId([0x0a, 0, 0, 0, 0, 0, 0, 0xff]);
assert_eq!(
id.to_string(),
"0a000000000000ff",
"two lower-case hex digits per byte, in order, zeros kept"
);
assert_eq!(
KeyId::parse("0a000000000000ff"),
Some(id),
"parse reads the spelling Display writes"
);
for bad in [
"0A000000000000FF",
"0a000000000000f",
"0a000000000000ff0",
"",
] {
assert_eq!(KeyId::parse(bad), None, "{bad:?}");
}
}
#[test]
fn ids_are_random() {
varies(&(0..8).map(|_| VersionId::generate().0).collect::<Vec<_>>());
varies(&(0..8).map(|_| TenantId::generate().0).collect::<Vec<_>>());
varies(&(0..8).map(|_| FileId::generate().0).collect::<Vec<_>>());
}
#[test]
fn a_file_id_parses_only_from_its_own_spelling() {
let mut bytes = [0; 16];
bytes[0] = 0x0a;
bytes[15] = 0xff;
assert_eq!(
FileId(bytes).to_string(),
format!("0a{}ff", "00".repeat(14)),
"two lower-case hex digits per byte, in order, zeros kept"
);
let file = FileId::generate();
assert_eq!(
FileId::parse(&file.to_string()),
Some(file),
"parse reads the spelling Display writes"
);
let text = file.to_string();
for bad in misspelled(&text) {
if bad != text {
assert_eq!(
FileId::parse(&bad),
None,
"not a file id's spelling: {bad:?}"
);
}
}
}
fn misspelled(text: &str) -> [String; 7] {
[
String::new(),
text[..31].to_owned(),
format!("{text}0"),
text.to_uppercase(),
format!("g{}", &text[1..]),
format!("{text}.tmp"),
format!("+{}", &text[1..]),
]
}
#[test]
fn tenant_and_version_ids_parse_only_from_their_own_spelling() {
let tenant = TenantId::generate();
let version = VersionId::generate();
assert_eq!(
TenantId::parse(&tenant.to_string()),
Some(tenant),
"parse reads the spelling Display writes"
);
assert_eq!(
VersionId::parse(&version.to_string()),
Some(version),
"parse reads the spelling Display writes"
);
for (text, parsed) in [
(
tenant.to_string(),
(|t: &str| TenantId::parse(t).is_some()) as fn(&str) -> bool,
),
(version.to_string(), |t: &str| VersionId::parse(t).is_some()),
] {
for bad in misspelled(&text) {
if bad != text {
assert!(!parsed(&bad), "not an id's own spelling: {bad:?}");
}
}
}
}
}