Skip to main content

dodecet_encoder/
hex.rs

1//! # Hex encoding/decoding utilities
2//!
3//! Efficient bidirectional conversion between dodecets and hex strings.
4
5use crate::{Dodecet, DodecetError, Result};
6
7/// Encode a slice of dodecets to a hex string
8///
9/// Each dodecet becomes 3 hex characters.
10///
11/// # Example
12///
13/// ```rust
14/// use dodecet_encoder::{Dodecet, hex};
15///
16/// let dodecets = vec![Dodecet::from_hex(0x123), Dodecet::from_hex(0x456)];
17/// let hex_str = hex::encode(&dodecets);
18/// assert_eq!(hex_str, "123456");
19/// ```
20pub fn encode(dodecets: &[Dodecet]) -> String {
21    dodecets
22        .iter()
23        .map(|d| d.to_hex_string())
24        .collect::<Vec<_>>()
25        .join("")
26}
27
28/// Decode a hex string to a vector of dodecets
29///
30/// # Errors
31/// Returns `DodecetError::InvalidHex` if:
32/// - String length is not a multiple of 3
33/// - Contains non-hex characters
34/// - Any value > 4095
35///
36/// # Example
37///
38/// ```rust
39/// use dodecet_encoder::hex;
40///
41/// let hex_str = "123456789";
42/// let dodecets = hex::decode(hex_str).unwrap();
43/// assert_eq!(dodecets.len(), 3);
44/// assert_eq!(dodecets[0].value(), 0x123);
45/// ```
46pub fn decode(s: &str) -> Result<Vec<Dodecet>> {
47    if !s.len().is_multiple_of(3) {
48        return Err(DodecetError::InvalidHex);
49    }
50
51    s.as_bytes()
52        .chunks(3)
53        .map(|chunk| {
54            let chunk_str = std::str::from_utf8(chunk).map_err(|_| DodecetError::InvalidHex)?;
55            Dodecet::from_hex_str(chunk_str)
56        })
57        .collect()
58}
59
60/// Encode a single dodecet to 3 hex characters
61///
62/// # Example
63///
64/// ```rust
65/// use dodecet_encoder::{Dodecet, hex};
66///
67/// let d = Dodecet::from_hex(0xAB0);
68/// assert_eq!(hex::encode_dodecet(d), "AB0");
69/// ```
70pub fn encode_dodecet(d: Dodecet) -> String {
71    d.to_hex_string()
72}
73
74/// Decode 3 hex characters to a single dodecet
75///
76/// # Example
77///
78/// ```rust
79/// use dodecet_encoder::{Dodecet, hex};
80///
81/// let d = hex::decode_dodecet("AB0").unwrap();
82/// assert_eq!(d.value(), 0xAB0);
83/// ```
84pub fn decode_dodecet(s: &str) -> Result<Dodecet> {
85    if s.len() != 3 {
86        return Err(DodecetError::InvalidHex);
87    }
88    Dodecet::from_hex_str(s)
89}
90
91/// Validate a hex string for dodecet encoding
92///
93/// Returns true if the string is valid for decoding.
94///
95/// # Example
96///
97/// ```rust
98/// use dodecet_encoder::hex;
99///
100/// assert!(hex::is_valid("123456"));
101/// assert!(!hex::is_valid("12345")); // Not multiple of 3
102/// assert!(!hex::is_valid("GHI")); // Non-hex characters
103/// ```
104pub fn is_valid(s: &str) -> bool {
105    if !s.len().is_multiple_of(3) {
106        return false;
107    }
108
109    s.chars()
110        .all(|c| c.is_ascii_hexdigit() && c.is_ascii_alphanumeric())
111}
112
113/// Format a hex string with spacing for readability
114///
115/// Adds a space every 3 characters (every dodecet).
116///
117/// # Example
118///
119/// ```rust
120/// use dodecet_encoder::hex;
121///
122/// let formatted = hex::format_spaced("123456789");
123/// assert_eq!(formatted, "123 456 789");
124/// ```
125pub fn format_spaced(s: &str) -> String {
126    s.as_bytes()
127        .chunks(3)
128        .map(std::str::from_utf8)
129        .collect::<std::result::Result<Vec<_>, _>>()
130        .map_err(|_| DodecetError::InvalidHex)
131        .unwrap()
132        .join(" ")
133}
134
135/// Remove spaces from a spaced hex string
136///
137/// # Example
138///
139/// ```rust
140/// use dodecet_encoder::hex;
141///
142/// let unspaced = hex::remove_spaces("123 456 789");
143/// assert_eq!(unspaced, "123456789");
144/// ```
145pub fn remove_spaces(s: &str) -> String {
146    s.chars().filter(|c| !c.is_whitespace()).collect()
147}
148
149/// Convert hex string to uppercase
150///
151/// # Example
152///
153/// ```rust
154/// use dodecet_encoder::hex;
155///
156/// assert_eq!(hex::to_uppercase("abc"), "ABC");
157/// ```
158pub fn to_uppercase(s: &str) -> String {
159    s.to_uppercase()
160}
161
162/// Convert hex string to lowercase
163///
164/// # Example
165///
166/// ```rust
167/// use dodecet_encoder::hex;
168///
169/// assert_eq!(hex::to_lowercase("ABC"), "abc");
170/// ```
171pub fn to_lowercase(s: &str) -> String {
172    s.to_lowercase()
173}
174
175/// Calculate the number of dodecets in a hex string
176///
177/// # Example
178///
179/// ```rust
180/// use dodecet_encoder::hex;
181///
182/// assert_eq!(hex::dodecet_count("123456789"), 3);
183/// ```
184pub fn dodecet_count(s: &str) -> usize {
185    s.len() / 3
186}
187
188/// Create a hex editor-friendly view
189///
190/// Shows offset, hex values, and ASCII representation.
191///
192/// # Example
193///
194/// ```rust
195/// use dodecet_encoder::hex;
196///
197/// let view = hex::hex_view("123456789ABCDEF");
198/// // Output includes offset, hex, and ASCII
199/// ```
200pub fn hex_view(s: &str) -> String {
201    let mut view = String::new();
202    let dodecets = decode(s).unwrap_or_default();
203
204    for (i, d) in dodecets.iter().enumerate() {
205        let offset = i * 3;
206        let hex_val = format!("{:03X}", d.value());
207        let ascii = dodecet_to_ascii(d);
208
209        view.push_str(&format!("{:08X}  {}  |{}|\n", offset, hex_val, ascii));
210    }
211
212    view
213}
214
215/// Convert a dodecet to an ASCII representation
216///
217/// Maps each nibble to a character.
218fn dodecet_to_ascii(d: &Dodecet) -> String {
219    let n0 = d.nibble(0).unwrap();
220    let n1 = d.nibble(1).unwrap();
221    let n2 = d.nibble(2).unwrap();
222
223    let to_char = |n: u8| -> char {
224        if (0x20..=0x7E).contains(&n) {
225            n as char
226        } else {
227            '.'
228        }
229    };
230
231    format!("{}{}{}", to_char(n2), to_char(n1), to_char(n0))
232}
233
234/// Compare two hex strings for equality (case-insensitive)
235///
236/// # Example
237///
238/// ```rust
239/// use dodecet_encoder::hex;
240///
241/// assert!(hex::equal_ignore_case("ABC123", "abc123"));
242/// ```
243pub fn equal_ignore_case(a: &str, b: &str) -> bool {
244    a.to_lowercase() == b.to_lowercase()
245}
246
247/// XOR two hex strings of equal length
248///
249/// # Example
250///
251/// ```rust
252/// use dodecet_encoder::hex;
253///
254/// let result = hex::xor("FFF", "123").unwrap();
255/// assert_eq!(result, "EDC");
256/// ```
257pub fn xor(a: &str, b: &str) -> Result<String> {
258    if a.len() != b.len() {
259        return Err(DodecetError::InvalidHex);
260    }
261
262    let d1 = decode(a)?;
263    let d2 = decode(b)?;
264
265    let result: Vec<Dodecet> = d1
266        .iter()
267        .zip(d2.iter())
268        .map(|(d1, d2)| d1.xor(*d2))
269        .collect();
270
271    Ok(encode(&result))
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_encode_decode() {
280        let dodecets = vec![Dodecet::from_hex(0x123), Dodecet::from_hex(0x456)];
281        let hex_str = encode(&dodecets);
282        assert_eq!(hex_str, "123456");
283
284        let decoded = decode(&hex_str).unwrap();
285        assert_eq!(decoded.len(), 2);
286        assert_eq!(decoded[0].value(), 0x123);
287        assert_eq!(decoded[1].value(), 0x456);
288    }
289
290    #[test]
291    fn test_encode_decode_dodecet() {
292        let d = Dodecet::from_hex(0xAB0);
293        assert_eq!(encode_dodecet(d), "AB0");
294
295        let d2 = decode_dodecet("AB0").unwrap();
296        assert_eq!(d2.value(), 0xAB0);
297    }
298
299    #[test]
300    fn test_is_valid() {
301        assert!(is_valid("123456"));
302        assert!(!is_valid("12345")); // Not multiple of 3
303        assert!(is_valid("")); // Empty string is valid
304    }
305
306    #[test]
307    fn test_format_spaced() {
308        assert_eq!(format_spaced("123456789"), "123 456 789");
309    }
310
311    #[test]
312    fn test_remove_spaces() {
313        assert_eq!(remove_spaces("123 456 789"), "123456789");
314    }
315
316    #[test]
317    fn test_uppercase_lowercase() {
318        assert_eq!(to_uppercase("abc"), "ABC");
319        assert_eq!(to_lowercase("ABC"), "abc");
320    }
321
322    #[test]
323    fn test_dodecet_count() {
324        assert_eq!(dodecet_count("123456789"), 3);
325        assert_eq!(dodecet_count(""), 0);
326    }
327
328    #[test]
329    fn test_equal_ignore_case() {
330        assert!(equal_ignore_case("ABC123", "abc123"));
331        assert!(!equal_ignore_case("ABC123", "ABC124"));
332    }
333
334    #[test]
335    fn test_xor() {
336        let result = xor("FFF", "123").unwrap();
337        assert_eq!(result, "EDC");
338
339        let result = xor("000", "000").unwrap();
340        assert_eq!(result, "000");
341    }
342
343    #[test]
344    fn test_invalid_hex() {
345        assert!(decode("GHI").is_err());
346        assert!(decode("12345").is_err());
347    }
348
349    #[test]
350    fn test_hex_view() {
351        let view = hex_view("123456");
352        assert!(view.contains("123"));
353        assert!(view.contains("456"));
354    }
355}