waddling_errors_hash/lib.rs
1//! Hash computation for waddling-errors diagnostic codes
2//!
3//! This crate provides hash computation utilities that can be used both at
4//! compile time (in proc macros) and at runtime. The hash function is designed
5//! to be:
6//!
7//! - **Deterministic**: Same input always produces same hash
8//! - **Compact**: 5-character base62 encoding (916M combinations)
9//! - **Fast**: Uses ahash for performance
10//! - **Safe**: Alphanumeric only, safe for all logging systems
11//! - **no_std compatible**: Works in constrained environments
12//!
13//! ## Usage in Proc Macros (Compile Time)
14//!
15//! ```ignore
16//! use waddling_errors_hash::compute_hash;
17//!
18//! let hash = compute_hash("E.AUTH.TOKEN.001");
19//! // Embed hash as a compile-time constant
20//! ```
21//!
22//! ## Usage at Runtime
23//!
24//! ```ignore
25//! use waddling_errors_hash::compute_hash;
26//!
27//! fn main() {
28//! let code = "E.AUTH.TOKEN.001";
29//! let hash = compute_hash(code);
30//! println!("Hash: {}", hash); // e.g., "aB3xY"
31//! }
32//! ```
33
34#![cfg_attr(not(feature = "std"), no_std)]
35
36#[cfg(not(feature = "std"))]
37extern crate alloc;
38
39#[cfg(feature = "std")]
40use std::string::String;
41
42#[cfg(not(feature = "std"))]
43use alloc::string::String;
44
45use ahash::RandomState;
46use core::hash::{BuildHasher, Hash, Hasher};
47
48/// Compute a 5-character base62 hash from an input string
49///
50/// Uses ahash with a fixed "Waddling" salt to ensure deterministic results
51/// across compilations and platforms. Returns an alphanumeric-only hash
52/// that is safe for all logging systems and provides 916M+ combinations.
53///
54/// # Algorithm
55///
56/// 1. Initialize ahash with fixed seeds (derived from "Waddling")
57/// 2. Hash the salt + input string
58/// 3. Take the first 5 bytes of the 64-bit hash
59/// 4. Convert to base62 (0-9, A-Z, a-z)
60///
61/// # Examples
62///
63/// ```
64/// use waddling_errors_hash::compute_hash;
65///
66/// let hash = compute_hash("E.AUTH.TOKEN.001");
67/// assert_eq!(hash.len(), 5);
68/// assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
69/// ```
70///
71/// # Determinism
72///
73/// The same input will always produce the same hash:
74///
75/// ```
76/// use waddling_errors_hash::compute_hash;
77///
78/// let hash1 = compute_hash("E.AUTH.TOKEN.001");
79/// let hash2 = compute_hash("E.AUTH.TOKEN.001");
80/// assert_eq!(hash1, hash2);
81/// ```
82pub fn compute_hash(input: &str) -> String {
83 const SALT: &str = "Waddling";
84
85 // Fixed seeds for deterministic hashing across compilations
86 // Derived from UTF-8 bytes of "Waddling"
87 let state = RandomState::with_seeds(
88 0x576164646c696e67, // "Waddling" as u64 (big-endian)
89 0x0000000000000000,
90 0x0000000000000000,
91 0x0000000000000000,
92 );
93
94 // Compute hash with salt + input
95 let mut hasher = state.build_hasher();
96 SALT.hash(&mut hasher);
97 input.hash(&mut hasher);
98 let hash_result = hasher.finish();
99
100 // Extract first 5 bytes from the 64-bit hash
101 let bytes = [
102 (hash_result >> 56) as u8,
103 (hash_result >> 48) as u8,
104 (hash_result >> 40) as u8,
105 (hash_result >> 32) as u8,
106 (hash_result >> 24) as u8,
107 ];
108
109 // Convert to base62 (0-9, A-Z, a-z)
110 const BASE62_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
111 const BASE: u64 = 62;
112
113 // Combine bytes into a single number
114 let mut num = 0u64;
115 for &byte in &bytes {
116 num = (num << 8) | (byte as u64);
117 }
118
119 // Convert to base62 with exactly 5 characters
120 let mut result_chars = [0u8; 5];
121 let mut n = num;
122
123 for i in (0..5).rev() {
124 result_chars[i] = BASE62_CHARS[(n % BASE) as usize];
125 n /= BASE;
126 }
127
128 // Safety: base62 encoding always produces valid UTF-8
129 String::from_utf8(result_chars.to_vec()).expect("base62 encoding produces valid UTF-8")
130}
131
132/// Verify that a hash was computed from the given input
133///
134/// This is useful for validating that a hash matches an error code,
135/// which can help detect mismatches or corruption.
136///
137/// # Examples
138///
139/// ```
140/// use waddling_errors_hash::{compute_hash, verify_hash};
141///
142/// let code = "E.AUTH.TOKEN.001";
143/// let hash = compute_hash(code);
144/// assert!(verify_hash(code, &hash));
145/// assert!(!verify_hash("E.AUTH.TOKEN.002", &hash));
146/// ```
147pub fn verify_hash(input: &str, hash: &str) -> bool {
148 let computed = compute_hash(input);
149 computed == hash
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_compute_hash_deterministic() {
158 let input = "E.AUTH.TOKEN.001";
159 let hash1 = compute_hash(input);
160 let hash2 = compute_hash(input);
161 assert_eq!(hash1, hash2, "Hash should be deterministic");
162 }
163
164 #[test]
165 fn test_hash_length() {
166 let hash = compute_hash("E.AUTH.TOKEN.001");
167 assert_eq!(hash.len(), 5, "Hash should be exactly 5 characters");
168 }
169
170 #[test]
171 fn test_hash_alphanumeric() {
172 let hash = compute_hash("E.AUTH.TOKEN.001");
173 assert!(
174 hash.chars().all(|c| c.is_ascii_alphanumeric()),
175 "Hash should contain only alphanumeric characters"
176 );
177 }
178
179 #[test]
180 fn test_different_inputs_produce_different_hashes() {
181 let hash1 = compute_hash("E.AUTH.TOKEN.001");
182 let hash2 = compute_hash("E.AUTH.TOKEN.002");
183 assert_ne!(
184 hash1, hash2,
185 "Different inputs should produce different hashes"
186 );
187 }
188
189 #[test]
190 fn test_verify_hash() {
191 let input = "E.AUTH.TOKEN.001";
192 let hash = compute_hash(input);
193 assert!(
194 verify_hash(input, &hash),
195 "Hash verification should succeed"
196 );
197 assert!(
198 !verify_hash("E.AUTH.TOKEN.002", &hash),
199 "Hash verification should fail for different input"
200 );
201 }
202
203 #[test]
204 fn test_hash_examples() {
205 // Test a few known inputs to ensure consistency across runs
206 let test_cases = vec![
207 "E.AUTH.TOKEN.001",
208 "W.DB.CONNECTION.002",
209 "I.API.RATE_LIMIT.003",
210 "E.CACHE.MISS.004",
211 ];
212
213 for input in test_cases {
214 let hash = compute_hash(input);
215 assert_eq!(hash.len(), 5);
216 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
217
218 // Verify it's reproducible
219 assert_eq!(hash, compute_hash(input));
220 }
221 }
222
223 #[test]
224 fn test_empty_string() {
225 let hash = compute_hash("");
226 assert_eq!(hash.len(), 5);
227 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
228 }
229
230 #[test]
231 fn test_unicode_input() {
232 let hash = compute_hash("E.AUTH.TOKEN.🦆");
233 assert_eq!(hash.len(), 5);
234 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
235 }
236
237 #[test]
238 fn test_long_input() {
239 let long_input = "E.AUTH.TOKEN.".to_string() + &"A".repeat(1000);
240 let hash = compute_hash(&long_input);
241 assert_eq!(hash.len(), 5);
242 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
243 }
244}