waddling_errors_hash/
lib.rs

1//! WDP-compliant hash computation for waddling-errors diagnostic codes
2//!
3//! This crate provides WDP-specified hash computation for error codes.
4//! Per WDP Part 5: Compact IDs, all error codes use xxHash3 with seed `0x000031762D706477`.
5//!
6//! ## Features
7//!
8//! - **WDP Compliant**: Uses xxHash3 with the standard WDP seed
9//! - **Deterministic**: Same input always produces same hash
10//! - **Compact**: 5-character base62 encoding (916M combinations)
11//! - **Fast**: xxHash3 is optimized for small inputs (~30 GB/s)
12//! - **Cross-language**: xxHash3 available in C, Python, JS, Go, Java, Rust
13//! - **no_std compatible**: Works in constrained environments
14//!
15//! ## Quick Start
16//!
17//! ```
18//! use waddling_errors_hash::{compute_hash, compute_wdp_hash};
19//!
20//! // Standard hash (no normalization)
21//! let hash = compute_hash("E.AUTH.TOKEN.001");
22//!
23//! // WDP-compliant hash (with case normalization)
24//! let wdp_hash = compute_wdp_hash("e.auth.token.001");
25//! ```
26//!
27//! ## WDP Specification
28//!
29//! Per WDP Part 5:
30//! - Algorithm: xxHash3
31//! - Seed: `0x000031762D706477` (ASCII "wdp-v1\0\0" as little-endian u64)
32//! - Output: 5 characters (base62)
33//! - Input normalization: uppercase for consistency
34
35#![cfg_attr(not(feature = "std"), no_std)]
36#![forbid(unsafe_code)]
37
38#[cfg(not(feature = "std"))]
39extern crate alloc;
40
41#[cfg(feature = "std")]
42use std::string::String;
43
44#[cfg(not(feature = "std"))]
45use alloc::string::String;
46
47// Public modules
48pub mod algorithm;
49pub mod base62;
50#[cfg(feature = "std")]
51pub mod config_loader;
52pub mod wdp; // WDP-conformant hash functions (Part 5 & 7)
53pub mod xxhash_impl;
54
55// Re-export public types
56pub use algorithm::{HashAlgorithm, HashConfig, ParseHashAlgorithmError, WDP_SEED, WDP_SEED_STR};
57pub use base62::{to_base62, u64_to_base62};
58#[cfg(feature = "std")]
59pub use config_loader::{DocGenConfig, apply_overrides, load_doc_gen_config, load_global_config};
60
61// Re-export namespace loader (std only)
62#[cfg(feature = "std")]
63pub use config_loader::load_namespace;
64
65// Re-export WDP-conformant functions
66pub use wdp::{
67    WDP_CODE_SEED, WDP_NAMESPACE_SEED, compute_wdp_full_id, compute_wdp_hash,
68    compute_wdp_namespace_hash, normalize_wdp_input, parse_wdp_full_id, verify_wdp_hash,
69    verify_wdp_namespace_hash,
70};
71
72// Re-export const (compile-time) WDP hash functions
73pub use wdp::{
74    const_format_sequence, const_hash_to_base62, const_wdp_hash, const_wdp_hash_bytes,
75    const_wdp_hash_from_parts,
76};
77
78/// Compute a 5-character base62 hash from an input string
79///
80/// Uses xxHash3 with the WDP seed `0x000031762D706477` to ensure deterministic
81/// results across compilations and platforms.
82///
83/// **Note:** This function does NOT normalize input (no uppercase conversion).
84/// For WDP-conformant hashing with normalization, use [`compute_wdp_hash`].
85///
86/// # Examples
87///
88/// ```
89/// use waddling_errors_hash::compute_hash;
90///
91/// let hash = compute_hash("E.AUTH.TOKEN.001");
92/// assert_eq!(hash.len(), 5);
93/// assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
94/// ```
95///
96/// # Determinism
97///
98/// The same input will always produce the same hash:
99///
100/// ```
101/// use waddling_errors_hash::compute_hash;
102///
103/// let hash1 = compute_hash("E.AUTH.TOKEN.001");
104/// let hash2 = compute_hash("E.AUTH.TOKEN.001");
105/// assert_eq!(hash1, hash2);
106/// ```
107pub fn compute_hash(input: &str) -> String {
108    compute_hash_with_config(input, &HashConfig::default())
109}
110
111/// Compute hash with custom configuration
112///
113/// Allows using a custom seed while still using xxHash3.
114///
115/// # Examples
116///
117/// ```
118/// use waddling_errors_hash::{compute_hash_with_config, HashConfig};
119///
120/// // Use a custom seed for isolated hash space
121/// let config = HashConfig::with_seed(0x12345678);
122/// let hash = compute_hash_with_config("E.AUTH.TOKEN.001", &config);
123/// assert_eq!(hash.len(), 5);
124/// ```
125pub fn compute_hash_with_config(input: &str, config: &HashConfig) -> String {
126    xxhash_impl::compute_xxhash3(input, config.seed)
127}
128
129/// Verify that a hash was computed from the given input
130///
131/// This is useful for validating that a hash matches an error code,
132/// which can help detect mismatches or corruption.
133///
134/// # Examples
135///
136/// ```
137/// use waddling_errors_hash::{compute_hash, verify_hash};
138///
139/// let code = "E.AUTH.TOKEN.001";
140/// let hash = compute_hash(code);
141/// assert!(verify_hash(code, &hash));
142/// assert!(!verify_hash("E.AUTH.TOKEN.002", &hash));
143/// ```
144pub fn verify_hash(input: &str, hash: &str) -> bool {
145    compute_hash(input) == hash
146}
147
148/// Verify that a hash was computed from the given input using custom config
149///
150/// # Examples
151///
152/// ```
153/// use waddling_errors_hash::{compute_hash_with_config, verify_hash_with_config, HashConfig};
154///
155/// let config = HashConfig::with_seed(0x12345678);
156/// let code = "E.AUTH.TOKEN.001";
157/// let hash = compute_hash_with_config(code, &config);
158/// assert!(verify_hash_with_config(code, &hash, &config));
159/// assert!(!verify_hash_with_config("E.AUTH.TOKEN.002", &hash, &config));
160/// ```
161pub fn verify_hash_with_config(input: &str, hash: &str, config: &HashConfig) -> bool {
162    compute_hash_with_config(input, config) == hash
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_compute_hash_default() {
171        let hash = compute_hash("E.AUTH.TOKEN.001");
172        assert_eq!(hash.len(), 5);
173        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
174    }
175
176    #[test]
177    fn test_compute_hash_deterministic() {
178        let hash1 = compute_hash("E.AUTH.TOKEN.001");
179        let hash2 = compute_hash("E.AUTH.TOKEN.001");
180        assert_eq!(hash1, hash2, "Hash should be deterministic");
181    }
182
183    #[test]
184    fn test_different_inputs_produce_different_hashes() {
185        let hash1 = compute_hash("E.AUTH.TOKEN.001");
186        let hash2 = compute_hash("E.AUTH.TOKEN.002");
187        assert_ne!(
188            hash1, hash2,
189            "Different inputs should produce different hashes"
190        );
191    }
192
193    #[test]
194    fn test_verify_hash() {
195        let input = "E.AUTH.TOKEN.001";
196        let hash = compute_hash(input);
197        assert!(
198            verify_hash(input, &hash),
199            "Hash verification should succeed"
200        );
201        assert!(
202            !verify_hash("E.AUTH.TOKEN.002", &hash),
203            "Hash verification should fail for different input"
204        );
205    }
206
207    #[test]
208    fn test_custom_seed() {
209        let input = "E.AUTH.TOKEN.001";
210        let config1 = HashConfig::with_seed(0x12345678);
211        let config2 = HashConfig::with_seed(0x87654321);
212
213        let hash1 = compute_hash_with_config(input, &config1);
214        let hash2 = compute_hash_with_config(input, &config2);
215
216        assert_ne!(
217            hash1, hash2,
218            "Different seeds should produce different hashes"
219        );
220    }
221
222    #[test]
223    fn test_verify_hash_with_config() {
224        let input = "E.AUTH.TOKEN.001";
225        let config = HashConfig::with_seed(0x12345678);
226        let hash = compute_hash_with_config(input, &config);
227
228        assert!(verify_hash_with_config(input, &hash, &config));
229        assert!(!verify_hash_with_config("E.AUTH.TOKEN.002", &hash, &config));
230    }
231
232    #[test]
233    fn test_empty_string() {
234        let hash = compute_hash("");
235        assert_eq!(hash.len(), 5);
236        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
237    }
238
239    #[test]
240    fn test_unicode_input() {
241        let hash = compute_hash("E.AUTH.TOKEN.🦆");
242        assert_eq!(hash.len(), 5);
243        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
244    }
245
246    #[test]
247    fn test_long_input() {
248        let long_input = "E.AUTH.TOKEN.".to_string() + &"A".repeat(1000);
249        let hash = compute_hash(&long_input);
250        assert_eq!(hash.len(), 5);
251        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
252    }
253
254    #[test]
255    fn test_wdp_seed_value() {
256        // Verify the WDP seed constant
257        assert_eq!(WDP_SEED, 0x000031762D706477);
258    }
259
260    #[test]
261    fn test_compute_hash_is_case_sensitive() {
262        // compute_hash does NOT normalize - it's case sensitive
263        let hash_mixed = compute_hash("E.Auth.Token.001");
264        let hash_upper = compute_hash("E.AUTH.TOKEN.001");
265
266        // These produce DIFFERENT hashes because compute_hash doesn't normalize
267        assert_ne!(
268            hash_mixed, hash_upper,
269            "compute_hash should be case-sensitive (no normalization)"
270        );
271    }
272
273    #[test]
274    fn test_compute_wdp_hash_is_case_insensitive() {
275        use wdp::compute_wdp_hash;
276
277        // compute_wdp_hash normalizes - it's case insensitive
278        let hash_mixed = compute_wdp_hash("E.Auth.Token.001");
279        let hash_upper = compute_wdp_hash("E.AUTH.TOKEN.001");
280
281        // These produce IDENTICAL hashes because compute_wdp_hash normalizes
282        assert_eq!(
283            hash_mixed, hash_upper,
284            "compute_wdp_hash should be case-insensitive (normalizes to uppercase)"
285        );
286    }
287}