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}