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