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
37#[cfg(not(feature = "std"))]
38extern crate alloc;
39
40#[cfg(feature = "std")]
41use std::string::String;
42
43#[cfg(not(feature = "std"))]
44use alloc::string::String;
45
46// Public modules
47pub mod algorithm;
48pub mod base62;
49#[cfg(feature = "std")]
50pub mod config_loader;
51pub mod wdp; // WDP-conformant hash functions (Part 5 & 7)
52pub mod xxhash_impl;
53
54// Re-export public types
55pub use algorithm::{HashAlgorithm, HashConfig, ParseHashAlgorithmError, WDP_SEED, WDP_SEED_STR};
56pub use base62::{to_base62, u64_to_base62};
57#[cfg(feature = "std")]
58pub use config_loader::{DocGenConfig, apply_overrides, load_doc_gen_config, load_global_config};
59
60// Re-export namespace loader (std only)
61#[cfg(feature = "std")]
62pub use config_loader::load_namespace;
63
64// Re-export WDP-conformant functions
65pub use wdp::{
66 WDP_CODE_SEED, WDP_NAMESPACE_SEED, compute_wdp_full_id, compute_wdp_hash,
67 compute_wdp_namespace_hash, normalize_wdp_input, parse_wdp_full_id, verify_wdp_hash,
68 verify_wdp_namespace_hash,
69};
70
71// Re-export const (compile-time) WDP hash functions
72pub use wdp::{
73 const_format_sequence, const_hash_to_base62, const_wdp_hash, const_wdp_hash_bytes,
74 const_wdp_hash_from_parts,
75};
76
77/// Compute a 5-character base62 hash from an input string
78///
79/// Uses xxHash3 with the WDP seed `0x000031762D706477` to ensure deterministic
80/// results across compilations and platforms.
81///
82/// **Note:** This function does NOT normalize input (no uppercase conversion).
83/// For WDP-conformant hashing with normalization, use [`compute_wdp_hash`].
84///
85/// # Examples
86///
87/// ```
88/// use waddling_errors_hash::compute_hash;
89///
90/// let hash = compute_hash("E.AUTH.TOKEN.001");
91/// assert_eq!(hash.len(), 5);
92/// assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
93/// ```
94///
95/// # Determinism
96///
97/// The same input will always produce the same hash:
98///
99/// ```
100/// use waddling_errors_hash::compute_hash;
101///
102/// let hash1 = compute_hash("E.AUTH.TOKEN.001");
103/// let hash2 = compute_hash("E.AUTH.TOKEN.001");
104/// assert_eq!(hash1, hash2);
105/// ```
106pub fn compute_hash(input: &str) -> String {
107 compute_hash_with_config(input, &HashConfig::default())
108}
109
110/// Compute hash with custom configuration
111///
112/// Allows using a custom seed while still using xxHash3.
113///
114/// # Examples
115///
116/// ```
117/// use waddling_errors_hash::{compute_hash_with_config, HashConfig};
118///
119/// // Use a custom seed for isolated hash space
120/// let config = HashConfig::with_seed(0x12345678);
121/// let hash = compute_hash_with_config("E.AUTH.TOKEN.001", &config);
122/// assert_eq!(hash.len(), 5);
123/// ```
124pub fn compute_hash_with_config(input: &str, config: &HashConfig) -> String {
125 xxhash_impl::compute_xxhash3(input, config.seed)
126}
127
128/// Verify that a hash was computed from the given input
129///
130/// This is useful for validating that a hash matches an error code,
131/// which can help detect mismatches or corruption.
132///
133/// # Examples
134///
135/// ```
136/// use waddling_errors_hash::{compute_hash, verify_hash};
137///
138/// let code = "E.AUTH.TOKEN.001";
139/// let hash = compute_hash(code);
140/// assert!(verify_hash(code, &hash));
141/// assert!(!verify_hash("E.AUTH.TOKEN.002", &hash));
142/// ```
143pub fn verify_hash(input: &str, hash: &str) -> bool {
144 compute_hash(input) == hash
145}
146
147/// Verify that a hash was computed from the given input using custom config
148///
149/// # Examples
150///
151/// ```
152/// use waddling_errors_hash::{compute_hash_with_config, verify_hash_with_config, HashConfig};
153///
154/// let config = HashConfig::with_seed(0x12345678);
155/// let code = "E.AUTH.TOKEN.001";
156/// let hash = compute_hash_with_config(code, &config);
157/// assert!(verify_hash_with_config(code, &hash, &config));
158/// assert!(!verify_hash_with_config("E.AUTH.TOKEN.002", &hash, &config));
159/// ```
160pub fn verify_hash_with_config(input: &str, hash: &str, config: &HashConfig) -> bool {
161 compute_hash_with_config(input, config) == hash
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_compute_hash_default() {
170 let hash = compute_hash("E.AUTH.TOKEN.001");
171 assert_eq!(hash.len(), 5);
172 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
173 }
174
175 #[test]
176 fn test_compute_hash_deterministic() {
177 let hash1 = compute_hash("E.AUTH.TOKEN.001");
178 let hash2 = compute_hash("E.AUTH.TOKEN.001");
179 assert_eq!(hash1, hash2, "Hash should be deterministic");
180 }
181
182 #[test]
183 fn test_different_inputs_produce_different_hashes() {
184 let hash1 = compute_hash("E.AUTH.TOKEN.001");
185 let hash2 = compute_hash("E.AUTH.TOKEN.002");
186 assert_ne!(
187 hash1, hash2,
188 "Different inputs should produce different hashes"
189 );
190 }
191
192 #[test]
193 fn test_verify_hash() {
194 let input = "E.AUTH.TOKEN.001";
195 let hash = compute_hash(input);
196 assert!(
197 verify_hash(input, &hash),
198 "Hash verification should succeed"
199 );
200 assert!(
201 !verify_hash("E.AUTH.TOKEN.002", &hash),
202 "Hash verification should fail for different input"
203 );
204 }
205
206 #[test]
207 fn test_custom_seed() {
208 let input = "E.AUTH.TOKEN.001";
209 let config1 = HashConfig::with_seed(0x12345678);
210 let config2 = HashConfig::with_seed(0x87654321);
211
212 let hash1 = compute_hash_with_config(input, &config1);
213 let hash2 = compute_hash_with_config(input, &config2);
214
215 assert_ne!(
216 hash1, hash2,
217 "Different seeds should produce different hashes"
218 );
219 }
220
221 #[test]
222 fn test_verify_hash_with_config() {
223 let input = "E.AUTH.TOKEN.001";
224 let config = HashConfig::with_seed(0x12345678);
225 let hash = compute_hash_with_config(input, &config);
226
227 assert!(verify_hash_with_config(input, &hash, &config));
228 assert!(!verify_hash_with_config("E.AUTH.TOKEN.002", &hash, &config));
229 }
230
231 #[test]
232 fn test_empty_string() {
233 let hash = compute_hash("");
234 assert_eq!(hash.len(), 5);
235 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
236 }
237
238 #[test]
239 fn test_unicode_input() {
240 let hash = compute_hash("E.AUTH.TOKEN.🦆");
241 assert_eq!(hash.len(), 5);
242 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
243 }
244
245 #[test]
246 fn test_long_input() {
247 let long_input = "E.AUTH.TOKEN.".to_string() + &"A".repeat(1000);
248 let hash = compute_hash(&long_input);
249 assert_eq!(hash.len(), 5);
250 assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
251 }
252
253 #[test]
254 fn test_wdp_seed_value() {
255 // Verify the WDP seed constant
256 assert_eq!(WDP_SEED, 0x000031762D706477);
257 }
258}