async_snmp/format/
hex.rs

1//! Hexadecimal encoding and decoding utilities.
2
3use std::fmt;
4
5/// Encode bytes as lowercase hex string.
6///
7/// # Examples
8///
9/// ```
10/// use async_snmp::format::hex::encode;
11///
12/// assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
13/// assert_eq!(encode(&[0x00, 0xff]), "00ff");
14/// ```
15pub fn encode(bytes: &[u8]) -> String {
16    bytes.iter().map(|b| format!("{:02x}", b)).collect()
17}
18
19/// Decode hex string to bytes.
20///
21/// Returns an error for invalid hex characters or odd-length strings.
22///
23/// # Examples
24///
25/// ```
26/// use async_snmp::format::hex::decode;
27///
28/// assert_eq!(decode("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
29/// assert_eq!(decode("00FF").unwrap(), vec![0x00, 0xff]);
30/// assert!(decode("xyz").is_err());
31/// assert!(decode("abc").is_err()); // odd length
32/// ```
33#[cfg(any(test, feature = "testing"))]
34pub fn decode(s: &str) -> Result<Vec<u8>, DecodeError> {
35    if !s.len().is_multiple_of(2) {
36        return Err(DecodeError::OddLength);
37    }
38    (0..s.len())
39        .step_by(2)
40        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| DecodeError::InvalidChar))
41        .collect()
42}
43
44/// Error type for hex decoding.
45#[cfg(any(test, feature = "testing"))]
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum DecodeError {
48    /// Input has odd length (must be pairs of hex digits)
49    OddLength,
50    /// Invalid hexadecimal character
51    InvalidChar,
52}
53
54/// Lazy hex formatter - only formats when actually displayed.
55///
56/// This avoids allocation when logging at disabled levels.
57///
58/// # Examples
59///
60/// ```
61/// use async_snmp::format::hex::Bytes;
62///
63/// let data = [0xde, 0xad, 0xbe, 0xef];
64/// let formatted = format!("{}", Bytes(&data));
65/// assert_eq!(formatted, "deadbeef");
66/// ```
67pub struct Bytes<'a>(pub &'a [u8]);
68
69impl fmt::Debug for Bytes<'_> {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        for b in self.0 {
72            write!(f, "{:02x}", b)?;
73        }
74        Ok(())
75    }
76}
77
78impl fmt::Display for Bytes<'_> {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        fmt::Debug::fmt(self, f)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_bytes_display() {
90        let data = [0xde, 0xad, 0xbe, 0xef];
91        let hex = Bytes(&data);
92        assert_eq!(format!("{}", hex), "deadbeef");
93    }
94
95    #[test]
96    fn test_bytes_debug() {
97        let data = [0x00, 0xff, 0x42];
98        let hex = Bytes(&data);
99        assert_eq!(format!("{:?}", hex), "00ff42");
100    }
101
102    #[test]
103    fn test_bytes_empty() {
104        let data: [u8; 0] = [];
105        let hex = Bytes(&data);
106        assert_eq!(format!("{}", hex), "");
107    }
108
109    #[test]
110    fn test_encode_basic() {
111        assert_eq!(encode(b"Hello world!"), "48656c6c6f20776f726c6421");
112        assert_eq!(encode(&[0x01, 0x02, 0x03, 0x0f, 0x10]), "0102030f10");
113    }
114
115    #[test]
116    fn test_encode_empty() {
117        assert_eq!(encode(&[]), "");
118    }
119
120    #[test]
121    fn test_encode_all_bytes() {
122        assert_eq!(encode(&[0x00]), "00");
123        assert_eq!(encode(&[0xff]), "ff");
124        assert_eq!(encode(&[0x00, 0xff]), "00ff");
125    }
126
127    #[test]
128    fn test_decode_basic() {
129        assert_eq!(decode("48656c6c6f20776f726c6421").unwrap(), b"Hello world!");
130        assert_eq!(
131            decode("0102030f10").unwrap(),
132            vec![0x01, 0x02, 0x03, 0x0f, 0x10]
133        );
134    }
135
136    #[test]
137    fn test_decode_empty() {
138        assert_eq!(decode("").unwrap(), Vec::<u8>::new());
139    }
140
141    #[test]
142    fn test_decode_mixed_case() {
143        assert_eq!(decode("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
144        assert_eq!(decode("DEADBEEF").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
145        assert_eq!(decode("DeAdBeEf").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
146    }
147
148    #[test]
149    fn test_decode_odd_length_error() {
150        assert_eq!(decode("1"), Err(DecodeError::OddLength));
151        assert_eq!(decode("123"), Err(DecodeError::OddLength));
152        assert_eq!(decode("12345"), Err(DecodeError::OddLength));
153    }
154
155    #[test]
156    fn test_decode_invalid_char_error() {
157        assert_eq!(decode("gg"), Err(DecodeError::InvalidChar));
158        assert_eq!(decode("0g"), Err(DecodeError::InvalidChar));
159        assert_eq!(decode("g0"), Err(DecodeError::InvalidChar));
160        assert_eq!(decode("xx"), Err(DecodeError::InvalidChar));
161        assert_eq!(decode("  "), Err(DecodeError::InvalidChar));
162    }
163
164    #[test]
165    fn test_roundtrip() {
166        let original = vec![
167            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
168            0xee, 0xff,
169        ];
170        let encoded = encode(&original);
171        let decoded = decode(&encoded).unwrap();
172        assert_eq!(original, decoded);
173    }
174}