use crate::{Transform, TransformError, TransformerCategory};
use std::fmt::Write;
const NAMESPACE_DNS: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
const NAMESPACE_URL: &str = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
const NAMESPACE_OID: &str = "6ba7b812-9dad-11d1-80b4-00c04fd430c8";
const NAMESPACE_X500: &str = "6ba7b814-9dad-11d1-80b4-00c04fd430c8";
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, TransformError> {
let hex = hex.replace('-', ""); if !hex.len().is_multiple_of(2) {
return Err(TransformError::InvalidArgument(
"Hex string must have even length".into(),
));
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
for i in (0..hex.len()).step_by(2) {
let byte_str = &hex[i..i + 2];
let byte = u8::from_str_radix(byte_str, 16)
.map_err(|_| TransformError::HexDecodeError("Invalid hex character".into()))?;
bytes.push(byte);
}
Ok(bytes)
}
fn sha1_hash(data: &[u8]) -> [u8; 20] {
let mut h0: u32 = 0x67452301;
let mut h1: u32 = 0xEFCDAB89;
let mut h2: u32 = 0x98BADCFE;
let mut h3: u32 = 0x10325476;
let mut h4: u32 = 0xC3D2E1F0;
let mut padded = data.to_vec();
let original_len_bits = (data.len() as u64) * 8;
padded.push(0x80);
while padded.len() % 64 != 56 {
padded.push(0);
}
padded.extend_from_slice(&original_len_bits.to_be_bytes());
for chunk_start in (0..padded.len()).step_by(64) {
let chunk = &padded[chunk_start..chunk_start + 64];
let mut w = [0u32; 80];
for (i, chunk_bytes) in chunk.chunks_exact(4).enumerate().take(16) {
w[i] = u32::from_be_bytes([
chunk_bytes[0],
chunk_bytes[1],
chunk_bytes[2],
chunk_bytes[3],
]);
}
for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}
let mut a = h0;
let mut b = h1;
let mut c = h2;
let mut d = h3;
let mut e = h4;
for (i, &word) in w.iter().enumerate() {
let (f, k) = match i {
0..=19 => ((b & c) | ((!b) & d), 0x5A827999),
20..=39 => (b ^ c ^ d, 0x6ED9EBA1),
40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC),
_ => (b ^ c ^ d, 0xCA62C1D6),
};
let temp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(word);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = temp;
}
h0 = h0.wrapping_add(a);
h1 = h1.wrapping_add(b);
h2 = h2.wrapping_add(c);
h3 = h3.wrapping_add(d);
h4 = h4.wrapping_add(e);
}
let mut result = [0u8; 20];
result[0..4].copy_from_slice(&h0.to_be_bytes());
result[4..8].copy_from_slice(&h1.to_be_bytes());
result[8..12].copy_from_slice(&h2.to_be_bytes());
result[12..16].copy_from_slice(&h3.to_be_bytes());
result[16..20].copy_from_slice(&h4.to_be_bytes());
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Uuid5Generate;
impl Uuid5Generate {
fn parse_namespace(namespace: &str) -> Result<[u8; 16], TransformError> {
let uuid_str = match namespace.to_lowercase().trim() {
"dns" | "namespace_dns" => NAMESPACE_DNS,
"url" | "namespace_url" => NAMESPACE_URL,
"oid" | "namespace_oid" => NAMESPACE_OID,
"x500" | "namespace_x500" => NAMESPACE_X500,
_ => namespace, };
if uuid_str.len() != 36
|| uuid_str.chars().nth(8) != Some('-')
|| uuid_str.chars().nth(13) != Some('-')
|| uuid_str.chars().nth(18) != Some('-')
|| uuid_str.chars().nth(23) != Some('-')
{
return Err(TransformError::InvalidArgument(
"Invalid namespace UUID format: must be in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".into(),
));
}
for (i, c) in uuid_str.chars().enumerate() {
if i == 8 || i == 13 || i == 18 || i == 23 {
if c != '-' {
return Err(TransformError::InvalidArgument(
"Invalid namespace UUID format: hyphens must be at positions 8, 13, 18, and 23".into(),
));
}
} else if !c.is_ascii_hexdigit() {
return Err(TransformError::InvalidArgument(
format!("Invalid namespace UUID format: character at position {} is not a valid hex digit", i).into(),
));
}
}
let bytes = hex_to_bytes(uuid_str)?;
if bytes.len() != 16 {
return Err(TransformError::InvalidArgument(
"Namespace UUID must be 16 bytes".into(),
));
}
let mut result = [0u8; 16];
result.copy_from_slice(&bytes);
Ok(result)
}
fn generate_v5_uuid(namespace: &[u8], name: &str) -> Result<String, TransformError> {
let mut input = Vec::with_capacity(namespace.len() + name.len());
input.extend_from_slice(namespace);
input.extend_from_slice(name.as_bytes());
let hash = sha1_hash(&input);
let mut uuid_bytes = [0u8; 16];
uuid_bytes.copy_from_slice(&hash[0..16]);
uuid_bytes[6] = (uuid_bytes[6] & 0x0f) | 0x50; uuid_bytes[8] = (uuid_bytes[8] & 0x3f) | 0x80;
let mut uuid_str = String::with_capacity(36);
write!(
&mut uuid_str,
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3],
uuid_bytes[4], uuid_bytes[5],
uuid_bytes[6], uuid_bytes[7],
uuid_bytes[8], uuid_bytes[9],
uuid_bytes[10], uuid_bytes[11], uuid_bytes[12], uuid_bytes[13], uuid_bytes[14], uuid_bytes[15]
).map_err(|e| TransformError::InvalidArgument(format!("Failed to format UUID: {}", e).into()))?;
Ok(uuid_str)
}
}
impl Transform for Uuid5Generate {
fn name(&self) -> &'static str {
"UUID v5 Generate (SHA-1, namespace-based)"
}
fn id(&self) -> &'static str {
"uuid5_generate"
}
fn description(&self) -> &'static str {
"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."
}
fn category(&self) -> TransformerCategory {
TransformerCategory::Crypto
}
fn transform(&self, input: &str) -> Result<String, TransformError> {
let parts: Vec<&str> = input.splitn(2, '|').collect(); if parts.len() != 2 {
return Err(TransformError::InvalidArgument(
"Input must be in the format 'namespace|name'. Namespace can be a UUID or one of: dns, url, oid, x500.".into()
));
}
let namespace_str = parts[0].trim();
let name = parts[1].trim();
let namespace_bytes = Self::parse_namespace(namespace_str)?;
Self::generate_v5_uuid(&namespace_bytes, name)
}
fn default_test_input(&self) -> &'static str {
"dns|example.com"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uuid5() {
let transformer = Uuid5Generate;
let result_default = transformer.transform(transformer.default_test_input());
assert!(result_default.is_ok());
assert_eq!(
result_default.unwrap(),
"cfbff0d1-9375-5685-968c-48ce8b15ae17"
);
let result_url = transformer.transform("url|http://example.com");
assert!(result_url.is_ok());
let uuid_url = result_url.unwrap();
assert_eq!(uuid_url.len(), 36);
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);
let result_custom = transformer.transform(&input_custom);
assert!(result_custom.is_ok());
let uuid_custom = result_custom.unwrap();
assert_eq!(uuid_custom.len(), 36);
assert!(uuid_custom.chars().nth(14) == Some('5'));
let result_x500 = transformer.transform("x500|o=example,c=us");
assert!(result_x500.is_ok());
let uuid_x500 = result_x500.unwrap();
assert_eq!(uuid_x500.len(), 36);
assert!(uuid_x500.chars().nth(14) == Some('5')); }
#[test]
fn test_uuid5_invalid_input() {
let transformer = Uuid5Generate;
let result = transformer.transform("invalid_input");
assert!(result.is_err());
let result = transformer.transform("invalid|name");
assert!(result.is_err());
}
#[test]
fn test_uuid5_deterministic() {
let transformer = Uuid5Generate;
let uuid1 = transformer
.transform(transformer.default_test_input())
.unwrap();
let uuid2 = transformer
.transform(transformer.default_test_input())
.unwrap();
assert_eq!(uuid1, uuid2);
let uuid3 = transformer.transform("dns|different.com").unwrap();
assert_ne!(uuid1, uuid3);
}
}