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