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}