1use sha2::{Digest, Sha256};
23
24pub const UUID_DOMAIN: &str = "ign-deploy:uuid:v1";
27
28pub fn name_based_uuid(gateway: &str, module: &str, ty: &str, name: &str) -> String {
31 let mut hasher = Sha256::new();
32 hasher.update(UUID_DOMAIN.as_bytes());
33 hasher.update([0u8]);
34 hasher.update(gateway.as_bytes());
35 hasher.update([0u8]);
36 hasher.update(module.as_bytes());
37 hasher.update(b"/");
38 hasher.update(ty.as_bytes());
39 hasher.update(b"/");
40 hasher.update(name.as_bytes());
41 let digest = hasher.finalize();
42 let mut b = [0u8; 16];
43 b.copy_from_slice(&digest[..16]);
44 b[6] = (b[6] & 0x0f) | 0x80;
45 b[8] = (b[8] & 0x3f) | 0x80;
46 let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
47 format!(
48 "{}-{}-{}-{}-{}",
49 &hex[0..8],
50 &hex[8..12],
51 &hex[12..16],
52 &hex[16..20],
53 &hex[20..32]
54 )
55}
56
57pub fn is_canonical_uuid(s: &str) -> bool {
60 s.len() == 36
61 && s.bytes().enumerate().all(|(i, c)| match i {
62 8 | 13 | 18 | 23 => c == b'-',
63 _ => c.is_ascii_hexdigit(),
64 })
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn name_based_uuid_matches_live_vector() {
73 assert_eq!(
76 name_based_uuid(
77 "ign-deploy-rig",
78 "ignition",
79 "database-translator",
80 "CAP21_V8"
81 ),
82 "4516ad9c-d6b0-8d86-89b9-802c96e33a9e"
83 );
84 assert_eq!(
86 name_based_uuid(
87 "ign-deploy-rig",
88 "ignition",
89 "database-translator",
90 "CAP21_STALE"
91 ),
92 "1caa175b-6703-89d8-8ead-c9b7c6bdfb96"
93 );
94 }
95
96 #[test]
97 fn uuid_is_version_8_and_rfc_variant() {
98 for name in ["a", "b", "Pump 1", "é", "x/y"] {
99 let u = name_based_uuid("g", "ignition", "schedule", name);
100 assert!(is_canonical_uuid(&u), "{u}");
101 assert_eq!(u, u.to_lowercase());
102 let c: Vec<char> = u.chars().collect();
103 assert_eq!(c[14], '8', "version nibble of {u}");
104 assert!("89ab".contains(c[19]), "variant nibble of {u}");
105 }
106 }
107
108 #[test]
109 fn uuid_is_deterministic_and_input_sensitive() {
110 let base = name_based_uuid("g", "m", "t", "n");
111 assert_eq!(base, name_based_uuid("g", "m", "t", "n"));
112 for other in [
113 name_based_uuid("g2", "m", "t", "n"),
114 name_based_uuid("g", "m2", "t", "n"),
115 name_based_uuid("g", "m", "t2", "n"),
116 name_based_uuid("g", "m", "t", "n2"),
117 ] {
118 assert_ne!(base, other);
119 }
120 assert_ne!(
121 name_based_uuid("g", "m", "t", "Pump"),
122 name_based_uuid("g", "m", "t", "pump")
123 );
124 assert_ne!(
126 name_based_uuid("g", "m", "t", "\u{e9}"),
127 name_based_uuid("g", "m", "t", "e\u{301}")
128 );
129 }
130
131 #[test]
132 fn uuid_fields_are_separated() {
133 assert_ne!(
134 name_based_uuid("ab", "c", "t", "n"),
135 name_based_uuid("a", "bc", "t", "n")
136 );
137 assert_ne!(
138 name_based_uuid("g", "m", "ab", "c"),
139 name_based_uuid("g", "m", "a", "bc")
140 );
141 }
142
143 #[test]
144 fn canonical_uuid_shape() {
145 assert!(is_canonical_uuid("4516ad9c-d6b0-8d86-89b9-802c96e33a9e"));
146 assert!(is_canonical_uuid("4516AD9C-D6B0-8D86-89B9-802C96E33A9E"));
147 for bad in [
148 "",
149 "4516ad9cd6b08d8689b9802c96e33a9e",
150 "4516ad9c-d6b0-8d86-89b9-802c96e33a9",
151 "4516ad9c-d6b0-8d86-89b9-802c96e33a9ee",
152 "4516ad9c-d6b0-8d86-89b9-802c96e33a9g",
153 "{4516ad9c-d6b0-8d86-89b9-802c96e33a9e}",
154 "4516ad9c_d6b0-8d86-89b9-802c96e33a9e",
155 ] {
156 assert!(!is_canonical_uuid(bad), "{bad:?}");
157 }
158 }
159}