use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyId(pub [u8; 8]);
impl KeyId {
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 {
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 {
pub fn generate() -> VersionId {
VersionId(random())
}
}
impl VersionId {
pub fn parse(text: &str) -> Option<VersionId> {
unhex(text).map(VersionId)
}
}
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)
}
}
#[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);
assert_eq!(VersionId(bytes).to_string(), expected);
}
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");
assert_eq!(KeyId::parse("0a000000000000ff"), Some(id));
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<_>>());
}
#[test]
fn a_tenant_id_parses_only_from_its_own_spelling() {
let id = TenantId::generate();
assert_eq!(TenantId::parse(&id.to_string()), Some(id));
let text = id.to_string();
for bad in [
text.to_uppercase(),
text[..31].to_owned(),
format!("{text}0"),
] {
if bad != text {
assert_eq!(TenantId::parse(&bad), None, "{bad:?}");
}
}
}
#[test]
fn a_version_id_parses_only_from_its_own_spelling() {
let id = VersionId::generate();
assert_eq!(VersionId::parse(&id.to_string()), Some(id));
let text = id.to_string();
let bad = [
String::new(),
text[..31].to_owned(),
format!("{text}0"),
text.to_uppercase(),
format!("g{}", &text[1..]),
format!("{}.tmp", text),
format!("+{}", &text[1..]),
];
for bad in bad {
if bad != text {
assert_eq!(VersionId::parse(&bad), None, "{bad:?}");
}
}
}
}