waddling_errors_hash/
wdp.rs

1//! WDP (Waddling Diagnostic Protocol) Conformant Hash Implementation
2//!
3//! This module provides hash functions that are fully conformant with the
4//! WDP specification (Parts 5 and 7).
5//!
6//! ## WDP Part 5: Compact IDs
7//!
8//! Error codes are hashed using xxHash3 with the seed `0x000031762D706477`:
9//! - Input is normalized (trimmed and uppercased)
10//! - Output is 5 Base62 characters
11//!
12//! ## WDP Part 7: Namespaces
13//!
14//! Namespaces are hashed using xxHash3 with a different seed `0x31762D736E706477`:
15//! - This prevents collisions between namespace and code hashes
16//! - Combined format: `namespace_hash-code_hash` (e.g., `h4tYw2-81E9g`)
17//!
18//! ## Example
19//!
20//! ```
21//! use waddling_errors_hash::wdp::{
22//!     compute_wdp_hash,
23//!     compute_wdp_namespace_hash,
24//!     compute_wdp_full_id,
25//! };
26//!
27//! // Hash an error code (WDP Part 5)
28//! let hash = compute_wdp_hash("E.Auth.Token.001");
29//! assert_eq!(hash.len(), 5);
30//!
31//! // Hash a namespace (WDP Part 7)
32//! let ns_hash = compute_wdp_namespace_hash("auth_lib");
33//! assert_eq!(ns_hash.len(), 5);
34//!
35//! // Combined identifier (namespace_hash-code_hash)
36//! let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
37//! assert_eq!(full_id.len(), 11); // 5 + 1 + 5
38//! assert!(full_id.contains('-'));
39//! ```
40
41#[cfg(not(feature = "std"))]
42extern crate alloc;
43
44#[cfg(feature = "std")]
45use std::string::String;
46
47#[cfg(not(feature = "std"))]
48use alloc::string::String;
49
50use crate::base62::to_base62;
51
52// =============================================================================
53// WDP Seed Constants (NORMATIVE)
54// =============================================================================
55
56/// WDP v1 seed for error code hashes (Part 5: Compact IDs)
57///
58/// Seed string: `"wdp-v1"`
59/// UTF-8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x76, 0x31]`
60/// Zero-padded to 8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x76, 0x31, 0x00, 0x00]`
61/// Little-endian u64: `0x000031762D706477`
62///
63/// This value is specified in WDP Part 5, Section 4.5.1.
64pub const WDP_CODE_SEED: u64 = 0x000031762D706477;
65
66/// WDP v1 seed for namespace hashes (Part 7: Namespaces)
67///
68/// Seed string: `"wdpns-v1"`
69/// UTF-8 bytes: `[0x77, 0x64, 0x70, 0x6E, 0x73, 0x2D, 0x76, 0x31]`
70/// Little-endian u64: `0x31762D736E706477`
71///
72/// This value is specified in WDP Part 7, Section 4.2.
73pub const WDP_NAMESPACE_SEED: u64 = 0x31762D736E706477;
74
75// =============================================================================
76// Input Normalization (WDP Part 5, Section 3)
77// =============================================================================
78
79/// Normalize input for WDP hash computation
80///
81/// Per WDP Part 5, Section 3.3:
82/// 1. Trim leading and trailing whitespace
83/// 2. Convert to uppercase
84///
85/// This ensures case-insensitive lookups and typo resilience:
86/// - `E.Auth.Token.001` → `E.AUTH.TOKEN.001`
87/// - `e.auth.token.001` → `E.AUTH.TOKEN.001`
88/// - `  E.Auth.Token.001  ` → `E.AUTH.TOKEN.001`
89///
90/// All produce the same hash.
91#[inline]
92pub fn normalize_wdp_input(input: &str) -> String {
93    input.trim().to_uppercase()
94}
95
96// =============================================================================
97// WDP Part 5: Compact ID Hash Functions
98// =============================================================================
99
100/// Compute a WDP-conformant hash for an error code (Part 5: Compact IDs)
101///
102/// This function implements the WDP Part 5 specification:
103/// 1. Normalize input (trim + uppercase)
104/// 2. Hash with xxHash3 using seed `0x000031762D706477`
105/// 3. Encode as 5-character Base62
106///
107/// # Example
108///
109/// ```
110/// use waddling_errors_hash::wdp::compute_wdp_hash;
111///
112/// // All of these produce the same hash due to normalization:
113/// let hash1 = compute_wdp_hash("E.Auth.Token.001");
114/// let hash2 = compute_wdp_hash("E.AUTH.TOKEN.001");
115/// let hash3 = compute_wdp_hash("e.auth.token.001");
116/// let hash4 = compute_wdp_hash("  E.Auth.Token.001  ");
117///
118/// assert_eq!(hash1, hash2);
119/// assert_eq!(hash2, hash3);
120/// assert_eq!(hash3, hash4);
121/// assert_eq!(hash1.len(), 5);
122/// ```
123///
124/// # WDP Conformance
125///
126/// This function is fully conformant with WDP Part 5: Compact IDs.
127pub fn compute_wdp_hash(input: &str) -> String {
128    use xxhash_rust::xxh3::xxh3_64_with_seed;
129
130    // Step 1: Normalize (trim + uppercase)
131    let normalized = normalize_wdp_input(input);
132
133    // Step 2: Hash with WDP seed
134    let hash = xxh3_64_with_seed(normalized.as_bytes(), WDP_CODE_SEED);
135
136    // Step 3: Convert to 5-char Base62
137    hash_to_base62(hash)
138}
139
140/// Compute WDP hash without normalization (for pre-normalized input)
141///
142/// Use this when you know the input is already normalized (uppercase, trimmed).
143/// This avoids the allocation from `to_uppercase()`.
144///
145/// # Safety Note
146///
147/// If the input is not properly normalized, the hash will differ from
148/// `compute_wdp_hash()` and may not match other WDP implementations.
149#[inline]
150pub fn compute_wdp_hash_normalized(normalized_input: &str) -> String {
151    use xxhash_rust::xxh3::xxh3_64_with_seed;
152
153    let hash = xxh3_64_with_seed(normalized_input.as_bytes(), WDP_CODE_SEED);
154    hash_to_base62(hash)
155}
156
157// =============================================================================
158// WDP Part 7: Namespace Hash Functions
159// =============================================================================
160
161/// Compute a WDP-conformant hash for a namespace (Part 7: Namespaces)
162///
163/// This function implements the WDP Part 7 specification:
164/// 1. Hash namespace with xxHash3 using seed `0x31762D736E706477`
165/// 2. Encode as 5-character Base62
166///
167/// Note: Namespaces are NOT normalized (they should already be lowercase snake_case).
168///
169/// # Example
170///
171/// ```
172/// use waddling_errors_hash::wdp::compute_wdp_namespace_hash;
173///
174/// let hash = compute_wdp_namespace_hash("auth_lib");
175/// assert_eq!(hash.len(), 5);
176/// ```
177///
178/// # WDP Conformance
179///
180/// This function is fully conformant with WDP Part 7: Namespaces.
181pub fn compute_wdp_namespace_hash(namespace: &str) -> String {
182    use xxhash_rust::xxh3::xxh3_64_with_seed;
183
184    // Namespace uses different seed to prevent collisions with code hashes
185    let hash = xxh3_64_with_seed(namespace.as_bytes(), WDP_NAMESPACE_SEED);
186    hash_to_base62(hash)
187}
188
189// =============================================================================
190// Combined Identifiers
191// =============================================================================
192
193/// Compute a full WDP identifier: `namespace_hash-code_hash`
194///
195/// Per WDP Part 7, Section 5.2, the combined format is:
196/// - `namespace_hash`: 5 Base62 characters
197/// - separator: single hyphen `-`
198/// - `code_hash`: 5 Base62 characters
199/// - Total: 11 characters
200///
201/// # Example
202///
203/// ```
204/// use waddling_errors_hash::wdp::compute_wdp_full_id;
205///
206/// let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
207/// assert_eq!(full_id.len(), 11);
208/// assert!(full_id.contains('-'));
209///
210/// // Format: "h4tYw2-81E9g" (namespace_hash-code_hash)
211/// let parts: Vec<&str> = full_id.split('-').collect();
212/// assert_eq!(parts.len(), 2);
213/// assert_eq!(parts[0].len(), 5); // namespace hash
214/// assert_eq!(parts[1].len(), 5); // code hash
215/// ```
216///
217/// # WDP Conformance
218///
219/// This function is fully conformant with WDP Part 5 and Part 7.
220pub fn compute_wdp_full_id(namespace: &str, code: &str) -> String {
221    let ns_hash = compute_wdp_namespace_hash(namespace);
222    let code_hash = compute_wdp_hash(code);
223
224    #[cfg(feature = "std")]
225    {
226        format!("{}-{}", ns_hash, code_hash)
227    }
228
229    #[cfg(not(feature = "std"))]
230    {
231        use alloc::format;
232        format!("{}-{}", ns_hash, code_hash)
233    }
234}
235
236/// Parse a WDP full identifier into namespace hash and code hash
237///
238/// Returns `Some((namespace_hash, code_hash))` if valid, `None` otherwise.
239///
240/// # Example
241///
242/// ```
243/// use waddling_errors_hash::wdp::parse_wdp_full_id;
244///
245/// // With namespace
246/// let result = parse_wdp_full_id("h4tYw-81E9g");
247/// assert_eq!(result, Some(("h4tYw", "81E9g")));
248///
249/// // Without namespace (legacy 5-char format)
250/// let result = parse_wdp_full_id("81E9g");
251/// assert_eq!(result, Some(("", "81E9g")));
252///
253/// // Invalid
254/// let result = parse_wdp_full_id("invalid");
255/// assert_eq!(result, None);
256/// ```
257pub fn parse_wdp_full_id(full_id: &str) -> Option<(&str, &str)> {
258    if full_id.len() == 5 && full_id.chars().all(|c| c.is_ascii_alphanumeric()) {
259        // Legacy format: just code hash
260        Some(("", full_id))
261    } else if full_id.len() == 11 {
262        // Full format: namespace_hash-code_hash (no allocation needed)
263        // Expected format: XXXXX-XXXXX where X is alphanumeric
264        let bytes = full_id.as_bytes();
265        if bytes[5] == b'-'
266            && full_id[..5].chars().all(|c| c.is_ascii_alphanumeric())
267            && full_id[6..].chars().all(|c| c.is_ascii_alphanumeric())
268        {
269            Some((&full_id[..5], &full_id[6..]))
270        } else {
271            None
272        }
273    } else {
274        None
275    }
276}
277
278// =============================================================================
279// Verification Functions
280// =============================================================================
281
282/// Verify that a hash matches the input code (WDP-conformant)
283///
284/// # Example
285///
286/// ```
287/// use waddling_errors_hash::wdp::{compute_wdp_hash, verify_wdp_hash};
288///
289/// let code = "E.Auth.Token.001";
290/// let hash = compute_wdp_hash(code);
291///
292/// assert!(verify_wdp_hash(code, &hash));
293/// assert!(verify_wdp_hash("e.auth.token.001", &hash)); // Case-insensitive!
294/// assert!(!verify_wdp_hash("E.Auth.Token.002", &hash));
295/// ```
296pub fn verify_wdp_hash(input: &str, hash: &str) -> bool {
297    compute_wdp_hash(input) == hash
298}
299
300/// Verify that a namespace hash matches the input namespace
301///
302/// # Example
303///
304/// ```
305/// use waddling_errors_hash::wdp::{compute_wdp_namespace_hash, verify_wdp_namespace_hash};
306///
307/// let namespace = "auth_lib";
308/// let hash = compute_wdp_namespace_hash(namespace);
309///
310/// assert!(verify_wdp_namespace_hash(namespace, &hash));
311/// assert!(!verify_wdp_namespace_hash("other_lib", &hash));
312/// ```
313pub fn verify_wdp_namespace_hash(namespace: &str, hash: &str) -> bool {
314    compute_wdp_namespace_hash(namespace) == hash
315}
316
317// =============================================================================
318// Compile-Time (const) Hash Functions
319// =============================================================================
320
321/// Base62 character set for const evaluation
322const BASE62_CHARS_CONST: &[u8; 62] =
323    b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
324
325/// Convert a u64 hash to a 5-character Base62 array (const-compatible)
326///
327/// This is a const fn version of hash_to_base62 that returns a fixed-size array
328/// instead of a String, making it suitable for compile-time evaluation.
329#[inline]
330pub const fn const_hash_to_base62(hash: u64) -> [u8; 5] {
331    // Take first 5 bytes (40 bits) of the hash
332    let bytes: [u8; 5] = [
333        (hash >> 32) as u8,
334        (hash >> 24) as u8,
335        (hash >> 16) as u8,
336        (hash >> 8) as u8,
337        hash as u8,
338    ];
339
340    // Combine into a 40-bit number
341    let mut num: u64 = 0;
342    num = (num << 8) | (bytes[0] as u64);
343    num = (num << 8) | (bytes[1] as u64);
344    num = (num << 8) | (bytes[2] as u64);
345    num = (num << 8) | (bytes[3] as u64);
346    num = (num << 8) | (bytes[4] as u64);
347
348    // Convert to base62
349    let mut result = [0u8; 5];
350    let mut n = num;
351
352    // Manual unrolling since we can't use for loops in const fn on mutable state easily
353    result[4] = BASE62_CHARS_CONST[(n % 62) as usize];
354    n /= 62;
355    result[3] = BASE62_CHARS_CONST[(n % 62) as usize];
356    n /= 62;
357    result[2] = BASE62_CHARS_CONST[(n % 62) as usize];
358    n /= 62;
359    result[1] = BASE62_CHARS_CONST[(n % 62) as usize];
360    n /= 62;
361    result[0] = BASE62_CHARS_CONST[(n % 62) as usize];
362
363    result
364}
365
366/// Compute WDP hash at compile time from a byte slice
367///
368/// This function uses const_xxh3 for compile-time hash computation.
369/// The input should be pre-normalized (uppercase ASCII bytes).
370///
371/// # Example
372///
373/// ```
374/// use waddling_errors_hash::wdp::{const_wdp_hash_bytes, const_hash_to_base62};
375///
376/// const HASH: u64 = const_wdp_hash_bytes(b"E.AUTH.TOKEN.001");
377/// const BASE62: [u8; 5] = const_hash_to_base62(HASH);
378/// ```
379#[inline]
380pub const fn const_wdp_hash_bytes(input: &[u8]) -> u64 {
381    use xxhash_rust::const_xxh3::xxh3_64_with_seed;
382    xxh3_64_with_seed(input, WDP_CODE_SEED)
383}
384
385/// Compute a WDP-conformant hash at compile time
386///
387/// This is a const fn that computes the hash from pre-normalized uppercase bytes.
388/// Returns a 5-byte array that can be converted to a &str.
389///
390/// # Example
391///
392/// ```
393/// use waddling_errors_hash::wdp::const_wdp_hash;
394///
395/// const HASH_BYTES: [u8; 5] = const_wdp_hash(b"E.AUTH.TOKEN.001");
396/// // Convert to str at compile time if needed
397/// ```
398#[inline]
399pub const fn const_wdp_hash(normalized_uppercase_input: &[u8]) -> [u8; 5] {
400    let hash = const_wdp_hash_bytes(normalized_uppercase_input);
401    const_hash_to_base62(hash)
402}
403
404/// Format a sequence number as 3-digit zero-padded string bytes
405///
406/// Returns a 3-byte array with the formatted number.
407/// Used for building WDP-conformant error codes at compile time.
408///
409/// # Panics
410///
411/// Panics if `seq_num` is 0 or greater than 999 (WDP spec requires 001-999).
412///
413/// # Example
414///
415/// ```
416/// use waddling_errors_hash::wdp::const_format_sequence;
417///
418/// assert_eq!(&const_format_sequence(1), b"001");
419/// assert_eq!(&const_format_sequence(42), b"042");
420/// assert_eq!(&const_format_sequence(999), b"999");
421/// ```
422#[inline]
423pub const fn const_format_sequence(seq_num: u16) -> [u8; 3] {
424    // WDP spec requires sequence numbers 001-999
425    assert!(
426        seq_num > 0,
427        "Sequence number cannot be 0. WDP spec requires 001-999."
428    );
429    assert!(
430        seq_num <= 999,
431        "Sequence number exceeds 999. WDP spec requires 001-999."
432    );
433
434    let d0 = (seq_num / 100) % 10;
435    let d1 = (seq_num / 10) % 10;
436    let d2 = seq_num % 10;
437
438    [b'0' + d0 as u8, b'0' + d1 as u8, b'0' + d2 as u8]
439}
440
441/// Compute WDP hash from error code parts at compile time
442///
443/// Takes the four parts of an error code (severity, component, primary, sequence)
444/// and computes the WDP-conformant hash. All string inputs must be uppercase.
445///
446/// # Arguments
447///
448/// * `severity` - Severity character as byte (e.g., b'E', b'W', b'C')
449/// * `component` - Component value as uppercase bytes (e.g., b"AUTH")
450/// * `primary` - Primary value as uppercase bytes (e.g., b"TOKEN")
451/// * `seq_num` - Sequence number (1-999)
452///
453/// # Example
454///
455/// ```
456/// use waddling_errors_hash::wdp::const_wdp_hash_from_parts;
457///
458/// // Hash for "E.AUTH.TOKEN.001"
459/// const HASH: [u8; 5] = const_wdp_hash_from_parts(b'E', b"AUTH", b"TOKEN", 1);
460/// ```
461#[inline]
462pub const fn const_wdp_hash_from_parts(
463    severity: u8,
464    component: &[u8],
465    primary: &[u8],
466    seq_num: u16,
467) -> [u8; 5] {
468    // Maximum expected length: 1 (severity) + 1 (.) + 16 (component) + 1 (.) + 16 (primary) + 1 (.) + 3 (seq) = 39
469    // Using 64 bytes for safety
470    let mut buffer = [0u8; 64];
471    let mut pos = 0;
472
473    // Severity
474    buffer[pos] = severity;
475    pos += 1;
476
477    // Dot
478    buffer[pos] = b'.';
479    pos += 1;
480
481    // Component (uppercase)
482    let mut i = 0;
483    while i < component.len() {
484        buffer[pos] = to_uppercase_byte(component[i]);
485        pos += 1;
486        i += 1;
487    }
488
489    // Dot
490    buffer[pos] = b'.';
491    pos += 1;
492
493    // Primary (uppercase)
494    i = 0;
495    while i < primary.len() {
496        buffer[pos] = to_uppercase_byte(primary[i]);
497        pos += 1;
498        i += 1;
499    }
500
501    // Dot
502    buffer[pos] = b'.';
503    pos += 1;
504
505    // Sequence number (3-digit zero-padded)
506    let seq_bytes = const_format_sequence(seq_num);
507    buffer[pos] = seq_bytes[0];
508    buffer[pos + 1] = seq_bytes[1];
509    buffer[pos + 2] = seq_bytes[2];
510    pos += 3;
511
512    // Compute hash on the constructed code string
513    const_wdp_hash(slice_to_len(&buffer, pos))
514}
515
516/// Convert a byte to uppercase (const-compatible)
517#[inline]
518const fn to_uppercase_byte(b: u8) -> u8 {
519    if b >= b'a' && b <= b'z' { b - 32 } else { b }
520}
521
522/// Create a slice of specific length from an array (const-compatible workaround)
523///
524/// Note: In const context, we can't return a slice directly from an array slice,
525/// so we use the hash function that takes the full buffer and length hint.
526#[inline]
527const fn slice_to_len(buffer: &[u8; 64], len: usize) -> &[u8] {
528    // Cap length to buffer bounds
529    let capped_len = if len > buffer.len() {
530        buffer.len()
531    } else {
532        len
533    };
534    // Safe slice indexing - works in const context since Rust 1.71
535    buffer.split_at(capped_len).0
536}
537
538// =============================================================================
539// Internal Helpers
540// =============================================================================
541
542/// Convert xxHash3 output to 5-character Base62
543///
544/// Takes the first 40 bits (5 bytes) of the hash and encodes as Base62.
545#[inline]
546fn hash_to_base62(hash: u64) -> String {
547    let bytes = [
548        (hash >> 32) as u8,
549        (hash >> 24) as u8,
550        (hash >> 16) as u8,
551        (hash >> 8) as u8,
552        hash as u8,
553    ];
554    to_base62(&bytes)
555}
556
557// =============================================================================
558// Tests
559// =============================================================================
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564
565    #[test]
566    fn test_wdp_code_seed_value() {
567        // Verify the seed constant matches the calculation
568        let seed_bytes = b"wdp-v1";
569        let mut padded = [0u8; 8];
570        padded[..6].copy_from_slice(seed_bytes);
571        let calculated = u64::from_le_bytes(padded);
572        assert_eq!(calculated, WDP_CODE_SEED);
573        assert_eq!(WDP_CODE_SEED, 0x000031762D706477);
574    }
575
576    #[test]
577    fn test_wdp_namespace_seed_value() {
578        // Verify the namespace seed constant matches the WDP spec
579        // Seed string: "wdpns-v1" (exactly 8 bytes)
580        // UTF-8 bytes: [0x77, 0x64, 0x70, 0x6E, 0x73, 0x2D, 0x76, 0x31]
581        // Little-endian u64: 0x31762D736E706477
582        let seed_bytes = b"wdpns-v1";
583        let calculated = u64::from_le_bytes(*seed_bytes);
584        assert_eq!(calculated, WDP_NAMESPACE_SEED);
585        assert_eq!(WDP_NAMESPACE_SEED, 0x31762D736E706477);
586    }
587
588    #[test]
589    fn test_normalize_wdp_input() {
590        assert_eq!(normalize_wdp_input("E.Auth.Token.001"), "E.AUTH.TOKEN.001");
591        assert_eq!(normalize_wdp_input("e.auth.token.001"), "E.AUTH.TOKEN.001");
592        assert_eq!(
593            normalize_wdp_input("  E.Auth.Token.001  "),
594            "E.AUTH.TOKEN.001"
595        );
596        assert_eq!(normalize_wdp_input("E.AUTH.TOKEN.001"), "E.AUTH.TOKEN.001");
597    }
598
599    #[test]
600    fn test_compute_wdp_hash_length() {
601        let hash = compute_wdp_hash("E.Auth.Token.001");
602        assert_eq!(hash.len(), 5);
603        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
604    }
605
606    #[test]
607    fn test_compute_wdp_hash_deterministic() {
608        let hash1 = compute_wdp_hash("E.Auth.Token.001");
609        let hash2 = compute_wdp_hash("E.Auth.Token.001");
610        assert_eq!(hash1, hash2);
611    }
612
613    #[test]
614    fn test_compute_wdp_hash_case_insensitive() {
615        let hash1 = compute_wdp_hash("E.Auth.Token.001");
616        let hash2 = compute_wdp_hash("E.AUTH.TOKEN.001");
617        let hash3 = compute_wdp_hash("e.auth.token.001");
618        let hash4 = compute_wdp_hash("E.auth.Token.001");
619
620        assert_eq!(hash1, hash2);
621        assert_eq!(hash2, hash3);
622        assert_eq!(hash3, hash4);
623    }
624
625    #[test]
626    fn test_compute_wdp_hash_whitespace_insensitive() {
627        let hash1 = compute_wdp_hash("E.Auth.Token.001");
628        let hash2 = compute_wdp_hash("  E.Auth.Token.001  ");
629        let hash3 = compute_wdp_hash("\tE.Auth.Token.001\n");
630
631        assert_eq!(hash1, hash2);
632        assert_eq!(hash2, hash3);
633    }
634
635    #[test]
636    fn test_different_codes_produce_different_hashes() {
637        let hash1 = compute_wdp_hash("E.Auth.Token.001");
638        let hash2 = compute_wdp_hash("E.Auth.Token.002");
639        assert_ne!(hash1, hash2);
640    }
641
642    #[test]
643    fn test_compute_wdp_namespace_hash_length() {
644        let hash = compute_wdp_namespace_hash("auth_lib");
645        assert_eq!(hash.len(), 5);
646        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
647    }
648
649    #[test]
650    fn test_namespace_and_code_seeds_differ() {
651        // Same input, different seeds should produce different hashes
652        let input = "test";
653        use xxhash_rust::xxh3::xxh3_64_with_seed;
654
655        let code_hash = xxh3_64_with_seed(input.as_bytes(), WDP_CODE_SEED);
656        let ns_hash = xxh3_64_with_seed(input.as_bytes(), WDP_NAMESPACE_SEED);
657
658        assert_ne!(code_hash, ns_hash);
659    }
660
661    #[test]
662    fn test_compute_wdp_full_id_format() {
663        let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
664
665        assert_eq!(full_id.len(), 11);
666        assert!(full_id.contains('-'));
667
668        let parts: Vec<&str> = full_id.split('-').collect();
669        assert_eq!(parts.len(), 2);
670        assert_eq!(parts[0].len(), 5);
671        assert_eq!(parts[1].len(), 5);
672    }
673
674    #[test]
675    fn test_compute_wdp_full_id_components() {
676        let namespace = "auth_lib";
677        let code = "E.Auth.Token.001";
678
679        let ns_hash = compute_wdp_namespace_hash(namespace);
680        let code_hash = compute_wdp_hash(code);
681        let full_id = compute_wdp_full_id(namespace, code);
682
683        let expected = format!("{}-{}", ns_hash, code_hash);
684        assert_eq!(full_id, expected);
685    }
686
687    #[test]
688    fn test_parse_wdp_full_id() {
689        // Full format
690        let result = parse_wdp_full_id("h4tYw-81E9g");
691        assert_eq!(result, Some(("h4tYw", "81E9g")));
692
693        // Legacy format (5-char)
694        let result = parse_wdp_full_id("81E9g");
695        assert_eq!(result, Some(("", "81E9g")));
696
697        // Invalid formats
698        assert_eq!(parse_wdp_full_id(""), None);
699        assert_eq!(parse_wdp_full_id("123"), None);
700        assert_eq!(parse_wdp_full_id("invalid-format-here"), None);
701        assert_eq!(parse_wdp_full_id("h4tYw--81E9g"), None);
702    }
703
704    #[test]
705    fn test_verify_wdp_hash() {
706        let code = "E.Auth.Token.001";
707        let hash = compute_wdp_hash(code);
708
709        assert!(verify_wdp_hash(code, &hash));
710        assert!(verify_wdp_hash("e.auth.token.001", &hash)); // Case-insensitive
711        assert!(!verify_wdp_hash("E.Auth.Token.002", &hash));
712    }
713
714    #[test]
715    fn test_verify_wdp_namespace_hash() {
716        let namespace = "auth_lib";
717        let hash = compute_wdp_namespace_hash(namespace);
718
719        assert!(verify_wdp_namespace_hash(namespace, &hash));
720        assert!(!verify_wdp_namespace_hash("other_lib", &hash));
721    }
722
723    // ==========================================================================
724    // Official WDP Test Vector Verification
725    // ==========================================================================
726    // These tests verify against official test vectors from wdp-specs repo:
727    // test-vectors/data/namespaces.json and test-vectors/data/compact-ids.json
728    // ==========================================================================
729
730    #[test]
731    fn test_wdp_official_namespace_vectors() {
732        // From wdp-specs/test-vectors/data/namespaces.json
733        let test_vectors = [
734            ("auth_lib", "05o5h"),
735            ("user_service", "oFN7q"),
736            ("order_service", "mulMW"),
737            ("my_app", "XPb13"),
738            ("a", "ECjXV"),
739            ("payment_gateway", "jJcx6"),
740            ("http2_client", "eXm0X"),
741            ("payment_lib", "MVYh6"),
742        ];
743
744        for (namespace, expected_hash) in test_vectors {
745            let actual = compute_wdp_namespace_hash(namespace);
746            assert_eq!(
747                actual, expected_hash,
748                "Namespace '{}' expected hash '{}' but got '{}'",
749                namespace, expected_hash, actual
750            );
751        }
752    }
753
754    #[test]
755    fn test_wdp_official_code_hash_vectors() {
756        // From wdp-specs/test-vectors/data/namespaces.json (combined_identifiers section)
757        // Code hashes are normalized (uppercased) before hashing
758        let test_vectors = [
759            ("E.Auth.Token.001", "V6a0B"),
760            ("E.Database.Connection.021", "gjyGQ"),
761        ];
762
763        for (code, expected_hash) in test_vectors {
764            let actual = compute_wdp_hash(code);
765            assert_eq!(
766                actual, expected_hash,
767                "Code '{}' expected hash '{}' but got '{}'",
768                code, expected_hash, actual
769            );
770        }
771    }
772
773    #[test]
774    fn test_wdp_official_combined_identifier_vectors() {
775        // From wdp-specs/test-vectors/data/namespaces.json
776        let test_vectors = [
777            ("auth_lib", "E.Auth.Token.001", "05o5h-V6a0B"),
778            ("payment_lib", "E.Auth.Token.001", "MVYh6-V6a0B"),
779            ("user_service", "E.Database.Connection.021", "oFN7q-gjyGQ"),
780        ];
781
782        for (namespace, code, expected_combined) in test_vectors {
783            let actual = compute_wdp_full_id(namespace, code);
784            assert_eq!(
785                actual, expected_combined,
786                "Combined ID for '{}:{}' expected '{}' but got '{}'",
787                namespace, code, expected_combined, actual
788            );
789        }
790    }
791}