waddling_errors_hash/
base62.rs

1//! Base62 encoding for hash output
2//!
3//! Converts raw hash bytes to a 5-character alphanumeric string.
4//! Base62 uses: 0-9, A-Z, a-z (62 characters total)
5//!
6//! This provides:
7//! - 62^5 = 916,132,832 possible combinations
8//! - Compact representation (5 characters)
9//! - Safe for all logging systems (alphanumeric only)
10//! - URL-safe (no special characters)
11
12#[cfg(not(feature = "std"))]
13extern crate alloc;
14
15#[cfg(feature = "std")]
16use std::string::String;
17
18#[cfg(not(feature = "std"))]
19use alloc::string::String;
20
21/// Base62 character set: 0-9, A-Z, a-z
22const BASE62_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
23const BASE: u64 = 62;
24
25/// Convert bytes to a 5-character base62 string
26///
27/// Takes the first 5 bytes from the input and converts them to a base62 string.
28/// If fewer than 5 bytes are provided, the remaining bytes are treated as 0.
29///
30/// # Examples
31///
32/// ```
33/// use waddling_errors_hash::to_base62;
34///
35/// let bytes = [0x12, 0x34, 0x56, 0x78, 0x9A];
36/// let result = to_base62(&bytes);
37/// assert_eq!(result.len(), 5);
38/// assert!(result.chars().all(|c| c.is_ascii_alphanumeric()));
39/// ```
40pub fn to_base62(bytes: &[u8]) -> String {
41    // Combine up to 5 bytes into a single u64
42    // This gives us 40 bits = 5 bytes
43    let mut num: u64 = 0;
44    let byte_count = bytes.len().min(5);
45
46    for &byte in bytes.iter().take(byte_count) {
47        num = (num << 8) | (byte as u64);
48    }
49
50    // Pad with zeros if we have fewer than 5 bytes
51    for _ in byte_count..5 {
52        num <<= 8;
53    }
54
55    // Convert to base62 with exactly 5 characters
56    let mut result_chars = [0u8; 5];
57    let mut n = num;
58
59    for i in (0..5).rev() {
60        result_chars[i] = BASE62_CHARS[(n % BASE) as usize];
61        n /= BASE;
62    }
63
64    // Safety: base62 encoding always produces valid UTF-8
65    String::from_utf8(result_chars.to_vec()).expect("base62 encoding produces valid UTF-8")
66}
67
68/// Convert a u64 value to a 5-character base62 string
69///
70/// Uses only the first 40 bits of the u64 value.
71///
72/// # Examples
73///
74/// ```
75/// use waddling_errors_hash::u64_to_base62;
76///
77/// let hash = u64_to_base62(123456789);
78/// assert_eq!(hash.len(), 5);
79/// ```
80pub fn u64_to_base62(value: u64) -> String {
81    let bytes = [
82        (value >> 32) as u8,
83        (value >> 24) as u8,
84        (value >> 16) as u8,
85        (value >> 8) as u8,
86        value as u8,
87    ];
88    to_base62(&bytes)
89}
90
91/// Seed a hash algorithm by converting a string seed to a u64
92///
93/// This is used for algorithms that need a numeric seed value.
94/// The conversion is deterministic and consistent across platforms.
95///
96/// # Examples
97///
98/// ```
99/// use waddling_errors_hash::base62::seed_to_u64;
100///
101/// let seed = seed_to_u64("wdp-v1");
102/// assert_eq!(seed, seed_to_u64("wdp-v1")); // Deterministic
103/// assert_eq!(seed, 0x000031762D706477); // WDP v1 seed
104/// ```
105pub fn seed_to_u64(seed: &str) -> u64 {
106    // WDP-conformant seed conversion:
107    // 1. Take UTF-8 bytes of seed string
108    // 2. Zero-pad to 8 bytes (on the right)
109    // 3. Interpret as little-endian u64
110    //
111    // Per WDP Part 5, Section 4.5.1
112    let seed_bytes = seed.as_bytes();
113    let mut padded = [0u8; 8];
114    let len = seed_bytes.len().min(8);
115    padded[..len].copy_from_slice(&seed_bytes[..len]);
116    u64::from_le_bytes(padded)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_to_base62_length() {
125        let bytes = [0x12, 0x34, 0x56, 0x78, 0x9A];
126        let result = to_base62(&bytes);
127        assert_eq!(result.len(), 5, "Result should be exactly 5 characters");
128    }
129
130    #[test]
131    fn test_to_base62_alphanumeric() {
132        let bytes = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
133        let result = to_base62(&bytes);
134        assert!(
135            result.chars().all(|c| c.is_ascii_alphanumeric()),
136            "Result should be alphanumeric only"
137        );
138    }
139
140    #[test]
141    fn test_to_base62_deterministic() {
142        let bytes = [0x12, 0x34, 0x56, 0x78, 0x9A];
143        let result1 = to_base62(&bytes);
144        let result2 = to_base62(&bytes);
145        assert_eq!(result1, result2, "Should be deterministic");
146    }
147
148    #[test]
149    fn test_to_base62_different_inputs() {
150        let bytes1 = [0x12, 0x34, 0x56, 0x78, 0x9A];
151        let bytes2 = [0x12, 0x34, 0x56, 0x78, 0x9B];
152        let result1 = to_base62(&bytes1);
153        let result2 = to_base62(&bytes2);
154        assert_ne!(
155            result1, result2,
156            "Different inputs should produce different outputs"
157        );
158    }
159
160    #[test]
161    fn test_to_base62_short_input() {
162        let bytes = [0x12];
163        let result = to_base62(&bytes);
164        assert_eq!(result.len(), 5, "Should pad to 5 characters");
165    }
166
167    #[test]
168    fn test_to_base62_empty_input() {
169        let bytes = [];
170        let result = to_base62(&bytes);
171        assert_eq!(result.len(), 5, "Should pad to 5 characters");
172    }
173
174    #[test]
175    fn test_u64_to_base62() {
176        let value = 123456789u64;
177        let result = u64_to_base62(value);
178        assert_eq!(result.len(), 5);
179        assert!(result.chars().all(|c| c.is_ascii_alphanumeric()));
180    }
181
182    #[test]
183    fn test_u64_to_base62_deterministic() {
184        let value = 987654321u64;
185        let result1 = u64_to_base62(value);
186        let result2 = u64_to_base62(value);
187        assert_eq!(result1, result2);
188    }
189
190    #[test]
191    fn test_seed_to_u64_deterministic() {
192        let seed = "wdp-v1";
193        let result1 = seed_to_u64(seed);
194        let result2 = seed_to_u64(seed);
195        assert_eq!(result1, result2, "Should be deterministic");
196    }
197
198    #[test]
199    fn test_seed_to_u64_wdp_conformant() {
200        // Verify the WDP seed produces the correct u64 value
201        // Per WDP Part 5, Section 4.5.1
202        let seed = "wdp-v1";
203        let result = seed_to_u64(seed);
204        assert_eq!(
205            result, 0x000031762D706477,
206            "WDP seed should match spec value"
207        );
208    }
209
210    #[test]
211    fn test_seed_to_u64_different_seeds() {
212        let seed1 = "wdp-v1";
213        let seed2 = "Different";
214        let result1 = seed_to_u64(seed1);
215        let result2 = seed_to_u64(seed2);
216        assert_ne!(
217            result1, result2,
218            "Different seeds should produce different values"
219        );
220    }
221
222    #[test]
223    fn test_seed_to_u64_empty() {
224        // Empty seed produces 0 (all zeros)
225        let seed = "";
226        let result = seed_to_u64(seed);
227        assert_eq!(result, 0, "Empty seed produces zero");
228    }
229}