use sha2::{Digest, Sha256};
pub const UUID_DOMAIN: &str = "ign-deploy:uuid:v1";
pub fn name_based_uuid(gateway: &str, module: &str, ty: &str, name: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(UUID_DOMAIN.as_bytes());
hasher.update([0u8]);
hasher.update(gateway.as_bytes());
hasher.update([0u8]);
hasher.update(module.as_bytes());
hasher.update(b"/");
hasher.update(ty.as_bytes());
hasher.update(b"/");
hasher.update(name.as_bytes());
let digest = hasher.finalize();
let mut b = [0u8; 16];
b.copy_from_slice(&digest[..16]);
b[6] = (b[6] & 0x0f) | 0x80;
b[8] = (b[8] & 0x3f) | 0x80;
let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
pub fn is_canonical_uuid(s: &str) -> bool {
s.len() == 36
&& s.bytes().enumerate().all(|(i, c)| match i {
8 | 13 | 18 | 23 => c == b'-',
_ => c.is_ascii_hexdigit(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_based_uuid_matches_live_vector() {
assert_eq!(
name_based_uuid(
"ign-deploy-rig",
"ignition",
"database-translator",
"CAP21_V8"
),
"4516ad9c-d6b0-8d86-89b9-802c96e33a9e"
);
assert_eq!(
name_based_uuid(
"ign-deploy-rig",
"ignition",
"database-translator",
"CAP21_STALE"
),
"1caa175b-6703-89d8-8ead-c9b7c6bdfb96"
);
}
#[test]
fn uuid_is_version_8_and_rfc_variant() {
for name in ["a", "b", "Pump 1", "é", "x/y"] {
let u = name_based_uuid("g", "ignition", "schedule", name);
assert!(is_canonical_uuid(&u), "{u}");
assert_eq!(u, u.to_lowercase());
let c: Vec<char> = u.chars().collect();
assert_eq!(c[14], '8', "version nibble of {u}");
assert!("89ab".contains(c[19]), "variant nibble of {u}");
}
}
#[test]
fn uuid_is_deterministic_and_input_sensitive() {
let base = name_based_uuid("g", "m", "t", "n");
assert_eq!(base, name_based_uuid("g", "m", "t", "n"));
for other in [
name_based_uuid("g2", "m", "t", "n"),
name_based_uuid("g", "m2", "t", "n"),
name_based_uuid("g", "m", "t2", "n"),
name_based_uuid("g", "m", "t", "n2"),
] {
assert_ne!(base, other);
}
assert_ne!(
name_based_uuid("g", "m", "t", "Pump"),
name_based_uuid("g", "m", "t", "pump")
);
assert_ne!(
name_based_uuid("g", "m", "t", "\u{e9}"),
name_based_uuid("g", "m", "t", "e\u{301}")
);
}
#[test]
fn uuid_fields_are_separated() {
assert_ne!(
name_based_uuid("ab", "c", "t", "n"),
name_based_uuid("a", "bc", "t", "n")
);
assert_ne!(
name_based_uuid("g", "m", "ab", "c"),
name_based_uuid("g", "m", "a", "bc")
);
}
#[test]
fn canonical_uuid_shape() {
assert!(is_canonical_uuid("4516ad9c-d6b0-8d86-89b9-802c96e33a9e"));
assert!(is_canonical_uuid("4516AD9C-D6B0-8D86-89B9-802C96E33A9E"));
for bad in [
"",
"4516ad9cd6b08d8689b9802c96e33a9e",
"4516ad9c-d6b0-8d86-89b9-802c96e33a9",
"4516ad9c-d6b0-8d86-89b9-802c96e33a9ee",
"4516ad9c-d6b0-8d86-89b9-802c96e33a9g",
"{4516ad9c-d6b0-8d86-89b9-802c96e33a9e}",
"4516ad9c_d6b0-8d86-89b9-802c96e33a9e",
] {
assert!(!is_canonical_uuid(bad), "{bad:?}");
}
}
}