use sha2::{Digest, Sha256};
use crate::error::SxurlError;
pub fn hash_component(label: &str, data: &[u8], bit_width: usize) -> Result<u64, SxurlError> {
if bit_width > 64 {
return Err(SxurlError::InternalError);
}
let mut hasher = Sha256::new();
hasher.update(label.as_bytes());
hasher.update(&[0x00]);
hasher.update(data);
let hash_result = hasher.finalize();
extract_lower_bits(&hash_result, bit_width)
}
pub fn extract_lower_bits(hash: &[u8], bit_width: usize) -> Result<u64, SxurlError> {
if bit_width > 64 {
return Err(SxurlError::InternalError);
}
let bytes = &hash[hash.len()-8..];
let value = u64::from_le_bytes(bytes.try_into().unwrap());
let mask = (1u64 << bit_width) - 1;
Ok(value & mask)
}
pub struct ComponentHasher;
impl ComponentHasher {
pub fn hash_tld(tld: &str) -> Result<u64, SxurlError> {
hash_component("tld", tld.as_bytes(), 28)
}
pub fn hash_domain(domain: &str) -> Result<u64, SxurlError> {
hash_component("domain", domain.as_bytes(), 60)
}
pub fn hash_subdomain(subdomain: &str) -> Result<u64, SxurlError> {
hash_component("subdomain", subdomain.as_bytes(), 32)
}
pub fn hash_path(path: &str) -> Result<u64, SxurlError> {
hash_component("path", path.as_bytes(), 52)
}
pub fn hash_params(params: &str) -> Result<u64, SxurlError> {
hash_component("query", params.as_bytes(), 32)
}
pub fn hash_fragment(fragment: &str) -> Result<u64, SxurlError> {
hash_component("fragment", fragment.as_bytes(), 20)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_hashing() {
let hash1 = hash_component("tld", b"com", 16).unwrap();
let hash2 = hash_component("tld", b"com", 16).unwrap();
assert_eq!(hash1, hash2);
}
#[test]
fn test_hash_extraction_verification() {
let known_hash = hex::decode("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08").unwrap();
let result_16 = extract_lower_bits(&known_hash, 16).unwrap();
println!("Known hash (last 4 bytes): {:02x}{:02x}{:02x}{:02x}",
known_hash[28], known_hash[29], known_hash[30], known_hash[31]);
println!("Extracted 16 bits (our method): 0x{:04x}", result_16);
let expected = 0x5dd1;
println!("Expected (last 8 bytes, little-endian, masked): 0x{:04x}", expected);
assert_eq!(result_16, expected, "Hash extraction should use corrected little-endian method");
}
#[test]
fn test_sxurl_test_vector_hash() {
let tld_hash = hash_component("tld", b"rs", 16).unwrap();
println!("H16(\"tld\", \"rs\") = 0x{:04x}", tld_hash);
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"tld");
hasher.update(&[0x00]);
hasher.update(b"rs");
let result = hasher.finalize();
println!("SHA256(\"tld\" || 0x00 || \"rs\") = {}", hex::encode(&result));
println!("Last 4 bytes: {:02x}{:02x}{:02x}{:02x}",
result[28], result[29], result[30], result[31]);
if tld_hash == 0x2397 {
println!("✓ Matches SXURL spec test vector");
} else {
println!("⚠ Does NOT match spec test vector 0x2397");
}
}
#[test]
fn test_different_labels_produce_different_hashes() {
let tld_hash = hash_component("tld", b"com", 16).unwrap();
let domain_hash = hash_component("domain", b"com", 16).unwrap();
assert_ne!(tld_hash, domain_hash);
}
#[test]
fn test_bit_width_limits() {
let hash_16 = hash_component("test", b"data", 16).unwrap();
let hash_32 = hash_component("test", b"data", 32).unwrap();
assert!(hash_16 < (1u64 << 16));
assert!(hash_32 < (1u64 << 32));
}
#[test]
fn test_component_hashers() {
assert!(ComponentHasher::hash_tld("com").is_ok());
assert!(ComponentHasher::hash_domain("example").is_ok());
assert!(ComponentHasher::hash_subdomain("api").is_ok());
assert!(ComponentHasher::hash_path("/search").is_ok());
assert!(ComponentHasher::hash_params("q=test").is_ok());
assert!(ComponentHasher::hash_fragment("results").is_ok());
}
#[test]
fn test_v2_bit_widths() {
let tld_hash = ComponentHasher::hash_tld("com").unwrap();
let path_hash = ComponentHasher::hash_path("/test").unwrap();
let params_hash = ComponentHasher::hash_params("q=test").unwrap();
let fragment_hash = ComponentHasher::hash_fragment("section").unwrap();
assert!(tld_hash < (1u64 << 28), "TLD hash should fit in 28 bits, got 0x{:x}", tld_hash);
assert!(path_hash < (1u64 << 52), "Path hash should fit in 52 bits, got 0x{:x}", path_hash);
assert!(params_hash < (1u64 << 32), "Params hash should fit in 32 bits, got 0x{:x}", params_hash);
assert!(fragment_hash < (1u64 << 20), "Fragment hash should fit in 20 bits, got 0x{:x}", fragment_hash);
println!("✓ v2 bit widths verified:");
println!(" TLD (28-bit): 0x{:07x}", tld_hash);
println!(" Path (52-bit): 0x{:013x}", path_hash);
println!(" Params (32-bit): 0x{:08x}", params_hash);
println!(" Fragment (20-bit): 0x{:05x}", fragment_hash);
}
#[test]
fn test_empty_components() {
assert!(ComponentHasher::hash_subdomain("").is_ok());
assert!(ComponentHasher::hash_params("").is_ok());
assert!(ComponentHasher::hash_fragment("").is_ok());
}
#[test]
fn test_extract_lower_bits() {
let test_hash = [0xFF, 0xAA, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00];
let bits_8 = extract_lower_bits(&test_hash, 8).unwrap();
assert_eq!(bits_8, 0xFF);
let bits_16 = extract_lower_bits(&test_hash, 16).unwrap();
assert_eq!(bits_16, 0xAAFF);
let bits_24 = extract_lower_bits(&test_hash, 24).unwrap();
assert_eq!(bits_24, 0x55AAFF); }
#[test]
fn test_known_hash_values() {
let com_hash = ComponentHasher::hash_tld("com").unwrap();
let rs_hash = ComponentHasher::hash_tld("rs").unwrap();
assert_ne!(com_hash, rs_hash);
assert!(com_hash < (1u64 << 28));
assert!(rs_hash < (1u64 << 28));
}
}