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}