Skip to main content

safe_decode/
hex.rs

1//! Hex rendering of a byte slice.
2
3use alloc::string::String;
4
5/// Render `bytes` as lowercase hex, two characters per byte, no separator.
6///
7/// ```
8/// use safe_decode::to_hex_lower;
9/// assert_eq!(to_hex_lower(&[0x00, 0xde, 0xad]), "00dead");
10/// ```
11#[must_use]
12pub fn to_hex_lower(bytes: &[u8]) -> String {
13    render(bytes, b'a')
14}
15
16/// Render `bytes` as uppercase hex, two characters per byte, no separator.
17///
18/// ```
19/// use safe_decode::to_hex_upper;
20/// assert_eq!(to_hex_upper(&[0x00, 0xde, 0xad]), "00DEAD");
21/// ```
22#[must_use]
23pub fn to_hex_upper(bytes: &[u8]) -> String {
24    render(bytes, b'A')
25}
26
27/// Render each byte as two hex digits, `ten` being the letter that stands for 0xA.
28fn render(bytes: &[u8], ten: u8) -> String {
29    let mut out = String::with_capacity(bytes.len().saturating_mul(2));
30    for &b in bytes {
31        out.push(digit(b >> 4, ten));
32        out.push(digit(b & 0x0F, ten));
33    }
34    out
35}
36
37/// One hex digit for the low nibble of `n`. Masking keeps the arithmetic in range, so no
38/// input can index or overflow out of the digit set.
39fn digit(n: u8, ten: u8) -> char {
40    match n & 0x0F {
41        d @ 0..=9 => char::from(b'0' + d),
42        d => char::from(ten + (d - 10)),
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::{to_hex_lower, to_hex_upper};
49    use alloc::vec::Vec;
50
51    #[test]
52    fn empty_input_yields_empty_output() {
53        assert_eq!(to_hex_lower(&[]), "");
54        assert_eq!(to_hex_upper(&[]), "");
55    }
56
57    #[test]
58    fn pads_every_byte_to_two_digits() {
59        // A leading zero nibble must survive; 0x0a is "0a", never "a".
60        assert_eq!(to_hex_lower(&[0x00, 0x01, 0x0a, 0x0f]), "00010a0f");
61    }
62
63    #[test]
64    fn renders_the_full_byte_range() {
65        let all: Vec<u8> = (0u8..=255).collect();
66        let lower = to_hex_lower(&all);
67        assert_eq!(lower.len(), 512);
68        assert!(lower.starts_with("000102"));
69        assert!(lower.ends_with("fdfeff"));
70        assert!(lower.chars().all(|c| c.is_ascii_hexdigit()));
71        assert!(lower.chars().all(|c| !c.is_ascii_uppercase()));
72    }
73
74    #[test]
75    fn upper_and_lower_differ_only_in_case() {
76        let bytes: &[u8] = &[0xde, 0xad, 0xbe, 0xef, 0x01, 0x23];
77        assert_eq!(to_hex_lower(bytes), "deadbeef0123");
78        assert_eq!(to_hex_upper(bytes), "DEADBEEF0123");
79        assert_eq!(
80            to_hex_upper(bytes).to_ascii_lowercase(),
81            to_hex_lower(bytes)
82        );
83    }
84
85    #[test]
86    fn output_length_is_always_twice_the_input() {
87        for n in 0..64usize {
88            let buf = alloc::vec![0xa5u8; n];
89            assert_eq!(to_hex_lower(&buf).len(), n * 2);
90            assert_eq!(to_hex_upper(&buf).len(), n * 2);
91        }
92    }
93}