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}