buup/transformers/
uuid5_generate.rs1use crate::{Transform, TransformError, TransformerCategory};
2use std::fmt::Write;
3
4const NAMESPACE_DNS: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
6const NAMESPACE_URL: &str = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
7const NAMESPACE_OID: &str = "6ba7b812-9dad-11d1-80b4-00c04fd430c8";
8const NAMESPACE_X500: &str = "6ba7b814-9dad-11d1-80b4-00c04fd430c8";
9
10fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, TransformError> {
12 let hex = hex.replace('-', ""); if !hex.len().is_multiple_of(2) {
14 return Err(TransformError::InvalidArgument(
15 "Hex string must have even length".into(),
16 ));
17 }
18
19 let mut bytes = Vec::with_capacity(hex.len() / 2);
20 for i in (0..hex.len()).step_by(2) {
21 let byte_str = &hex[i..i + 2];
22 let byte = u8::from_str_radix(byte_str, 16)
23 .map_err(|_| TransformError::HexDecodeError("Invalid hex character".into()))?;
24 bytes.push(byte);
25 }
26 Ok(bytes)
27}
28
29fn sha1_hash(data: &[u8]) -> [u8; 20] {
32 let mut h0: u32 = 0x67452301;
34 let mut h1: u32 = 0xEFCDAB89;
35 let mut h2: u32 = 0x98BADCFE;
36 let mut h3: u32 = 0x10325476;
37 let mut h4: u32 = 0xC3D2E1F0;
38
39 let mut padded = data.to_vec();
41 let original_len_bits = (data.len() as u64) * 8;
42
43 padded.push(0x80);
45
46 while padded.len() % 64 != 56 {
48 padded.push(0);
49 }
50
51 padded.extend_from_slice(&original_len_bits.to_be_bytes());
53
54 for chunk_start in (0..padded.len()).step_by(64) {
56 let chunk = &padded[chunk_start..chunk_start + 64];
57
58 let mut w = [0u32; 80];
60
61 for (i, chunk_bytes) in chunk.chunks_exact(4).enumerate().take(16) {
63 w[i] = u32::from_be_bytes([
64 chunk_bytes[0],
65 chunk_bytes[1],
66 chunk_bytes[2],
67 chunk_bytes[3],
68 ]);
69 }
70
71 for i in 16..80 {
73 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
74 }
75
76 let mut a = h0;
78 let mut b = h1;
79 let mut c = h2;
80 let mut d = h3;
81 let mut e = h4;
82
83 for (i, &word) in w.iter().enumerate() {
85 let (f, k) = match i {
86 0..=19 => ((b & c) | ((!b) & d), 0x5A827999),
87 20..=39 => (b ^ c ^ d, 0x6ED9EBA1),
88 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC),
89 _ => (b ^ c ^ d, 0xCA62C1D6),
90 };
91
92 let temp = a
93 .rotate_left(5)
94 .wrapping_add(f)
95 .wrapping_add(e)
96 .wrapping_add(k)
97 .wrapping_add(word);
98
99 e = d;
100 d = c;
101 c = b.rotate_left(30);
102 b = a;
103 a = temp;
104 }
105
106 h0 = h0.wrapping_add(a);
108 h1 = h1.wrapping_add(b);
109 h2 = h2.wrapping_add(c);
110 h3 = h3.wrapping_add(d);
111 h4 = h4.wrapping_add(e);
112 }
113
114 let mut result = [0u8; 20];
116 result[0..4].copy_from_slice(&h0.to_be_bytes());
117 result[4..8].copy_from_slice(&h1.to_be_bytes());
118 result[8..12].copy_from_slice(&h2.to_be_bytes());
119 result[12..16].copy_from_slice(&h3.to_be_bytes());
120 result[16..20].copy_from_slice(&h4.to_be_bytes());
121
122 result
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct Uuid5Generate;
128
129impl Uuid5Generate {
130 fn parse_namespace(namespace: &str) -> Result<[u8; 16], TransformError> {
131 let uuid_str = match namespace.to_lowercase().trim() {
133 "dns" | "namespace_dns" => NAMESPACE_DNS,
134 "url" | "namespace_url" => NAMESPACE_URL,
135 "oid" | "namespace_oid" => NAMESPACE_OID,
136 "x500" | "namespace_x500" => NAMESPACE_X500,
137 _ => namespace, };
139
140 if uuid_str.len() != 36
142 || uuid_str.chars().nth(8) != Some('-')
143 || uuid_str.chars().nth(13) != Some('-')
144 || uuid_str.chars().nth(18) != Some('-')
145 || uuid_str.chars().nth(23) != Some('-')
146 {
147 return Err(TransformError::InvalidArgument(
148 "Invalid namespace UUID format: must be in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".into(),
149 ));
150 }
151
152 for (i, c) in uuid_str.chars().enumerate() {
154 if i == 8 || i == 13 || i == 18 || i == 23 {
155 if c != '-' {
156 return Err(TransformError::InvalidArgument(
157 "Invalid namespace UUID format: hyphens must be at positions 8, 13, 18, and 23".into(),
158 ));
159 }
160 } else if !c.is_ascii_hexdigit() {
161 return Err(TransformError::InvalidArgument(
162 format!("Invalid namespace UUID format: character at position {} is not a valid hex digit", i).into(),
163 ));
164 }
165 }
166
167 let bytes = hex_to_bytes(uuid_str)?;
168 if bytes.len() != 16 {
169 return Err(TransformError::InvalidArgument(
170 "Namespace UUID must be 16 bytes".into(),
171 ));
172 }
173
174 let mut result = [0u8; 16];
175 result.copy_from_slice(&bytes);
176 Ok(result)
177 }
178
179 fn generate_v5_uuid(namespace: &[u8], name: &str) -> Result<String, TransformError> {
180 let mut input = Vec::with_capacity(namespace.len() + name.len());
182 input.extend_from_slice(namespace);
183 input.extend_from_slice(name.as_bytes());
184
185 let hash = sha1_hash(&input);
187
188 let mut uuid_bytes = [0u8; 16];
190 uuid_bytes.copy_from_slice(&hash[0..16]);
191
192 uuid_bytes[6] = (uuid_bytes[6] & 0x0f) | 0x50; uuid_bytes[8] = (uuid_bytes[8] & 0x3f) | 0x80; let mut uuid_str = String::with_capacity(36);
198 write!(
199 &mut uuid_str,
200 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
201 uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3],
202 uuid_bytes[4], uuid_bytes[5],
203 uuid_bytes[6], uuid_bytes[7],
204 uuid_bytes[8], uuid_bytes[9],
205 uuid_bytes[10], uuid_bytes[11], uuid_bytes[12], uuid_bytes[13], uuid_bytes[14], uuid_bytes[15]
206 ).map_err(|e| TransformError::InvalidArgument(format!("Failed to format UUID: {}", e).into()))?;
207
208 Ok(uuid_str)
209 }
210}
211
212impl Transform for Uuid5Generate {
213 fn name(&self) -> &'static str {
214 "UUID v5 Generate (SHA-1, namespace-based)"
215 }
216
217 fn id(&self) -> &'static str {
218 "uuid5_generate"
219 }
220
221 fn description(&self) -> &'static str {
222 "Generates a version 5 UUID based on namespace and name using SHA-1. Input format: \"namespace|name\". Namespace can be a UUID or one of: dns, url, oid, x500."
223 }
224
225 fn category(&self) -> TransformerCategory {
226 TransformerCategory::Crypto
227 }
228
229 fn transform(&self, input: &str) -> Result<String, TransformError> {
230 let parts: Vec<&str> = input.splitn(2, '|').collect(); if parts.len() != 2 {
233 return Err(TransformError::InvalidArgument(
234 "Input must be in the format 'namespace|name'. Namespace can be a UUID or one of: dns, url, oid, x500.".into()
235 ));
236 }
237
238 let namespace_str = parts[0].trim();
239 let name = parts[1].trim();
240
241 let namespace_bytes = Self::parse_namespace(namespace_str)?;
243
244 Self::generate_v5_uuid(&namespace_bytes, name)
246 }
247
248 fn default_test_input(&self) -> &'static str {
249 "dns|example.com"
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_uuid5() {
259 let transformer = Uuid5Generate;
260
261 let result_default = transformer.transform(transformer.default_test_input());
263 assert!(result_default.is_ok());
264 assert_eq!(
266 result_default.unwrap(),
267 "cfbff0d1-9375-5685-968c-48ce8b15ae17"
268 );
269
270 let result_url = transformer.transform("url|http://example.com");
272 assert!(result_url.is_ok());
273 let uuid_url = result_url.unwrap();
281 assert_eq!(uuid_url.len(), 36);
282 assert!(uuid_url.chars().nth(14) == Some('5')); let custom_namespace = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"; let input_custom = format!("{}|my custom name", custom_namespace);
287 let result_custom = transformer.transform(&input_custom);
288 assert!(result_custom.is_ok());
289 let uuid_custom = result_custom.unwrap();
292 assert_eq!(uuid_custom.len(), 36);
293 assert!(uuid_custom.chars().nth(14) == Some('5')); let result_x500 = transformer.transform("x500|o=example,c=us");
298 assert!(result_x500.is_ok());
299 let uuid_x500 = result_x500.unwrap();
301 assert_eq!(uuid_x500.len(), 36);
302 assert!(uuid_x500.chars().nth(14) == Some('5')); }
308
309 #[test]
310 fn test_uuid5_invalid_input() {
311 let transformer = Uuid5Generate;
312
313 let result = transformer.transform("invalid_input");
315 assert!(result.is_err());
316
317 let result = transformer.transform("invalid|name");
319 assert!(result.is_err());
320 }
321
322 #[test]
323 fn test_uuid5_deterministic() {
324 let transformer = Uuid5Generate;
325
326 let uuid1 = transformer
328 .transform(transformer.default_test_input())
329 .unwrap();
330 let uuid2 = transformer
331 .transform(transformer.default_test_input())
332 .unwrap();
333
334 assert_eq!(uuid1, uuid2);
335
336 let uuid3 = transformer.transform("dns|different.com").unwrap();
338 assert_ne!(uuid1, uuid3);
339 }
340}