Skip to main content

async_snmp/format/
hex.rs

1//! Hexadecimal encoding and decoding utilities.
2
3use std::fmt;
4use std::fmt::Write;
5
6const HEX_TABLE: &[u8; 16] = b"0123456789abcdef";
7
8/// Encode bytes as lowercase hex string.
9///
10/// # Examples
11///
12/// ```
13/// use async_snmp::format::hex::encode;
14///
15/// assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
16/// assert_eq!(encode(&[0x00, 0xff]), "00ff");
17/// ```
18#[must_use]
19pub fn encode(bytes: &[u8]) -> String {
20    let mut out = String::with_capacity(bytes.len() * 2);
21    write_to(&mut out, bytes);
22    out
23}
24
25/// Write lowercase hex-encoded bytes into an existing String buffer.
26///
27/// More efficient than [`encode`] when appending to an existing string.
28pub fn write_to(out: &mut String, bytes: &[u8]) {
29    for &b in bytes {
30        out.push(HEX_TABLE[(b >> 4) as usize] as char);
31        out.push(HEX_TABLE[(b & 0x0f) as usize] as char);
32    }
33}
34
35/// Decode hex string to bytes.
36///
37/// Returns an error for invalid hex characters or odd-length strings.
38///
39/// # Examples
40///
41/// ```
42/// use async_snmp::format::hex::decode;
43///
44/// assert_eq!(decode("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
45/// assert_eq!(decode("00FF").unwrap(), vec![0x00, 0xff]);
46/// assert!(decode("xyz").is_err());
47/// assert!(decode("abc").is_err()); // odd length
48/// ```
49pub fn decode(s: &str) -> Result<Vec<u8>, DecodeError> {
50    if !s.len().is_multiple_of(2) {
51        return Err(DecodeError::OddLength);
52    }
53    (0..s.len())
54        .step_by(2)
55        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| DecodeError::InvalidChar))
56        .collect()
57}
58
59/// Decode hex string to bytes, stripping spaces, colons, and dashes first.
60///
61/// Accepts formats like "de:ad:be:ef", "de-ad-be-ef", or "de ad be ef".
62///
63/// Returns an error if the stripped input has an odd number of hex digits or
64/// contains invalid hex characters.
65///
66/// # Examples
67///
68/// ```
69/// use async_snmp::format::hex::decode_relaxed;
70///
71/// assert_eq!(decode_relaxed("de:ad:be:ef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
72/// assert_eq!(decode_relaxed("de ad be ef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
73/// assert_eq!(decode_relaxed("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
74/// assert!(decode_relaxed("a").is_err()); // one hex digit is odd-length
75/// ```
76pub fn decode_relaxed(s: &str) -> Result<Vec<u8>, DecodeError> {
77    let clean: String = s.chars().filter(char::is_ascii_hexdigit).collect();
78    decode(&clean)
79}
80
81/// Check if bytes are printable ASCII or valid UTF-8 with only printable characters.
82///
83/// Returns `true` for empty slices.
84///
85/// # Examples
86///
87/// ```
88/// use async_snmp::format::hex::is_printable;
89///
90/// assert!(is_printable(b"Hello World"));
91/// assert!(is_printable(b""));
92/// assert!(!is_printable(&[0x00, 0x01]));
93/// assert!(!is_printable(&[0x80, 0x81]));
94/// ```
95#[must_use]
96pub fn is_printable(bytes: &[u8]) -> bool {
97    if bytes.is_empty() {
98        return true;
99    }
100    match std::str::from_utf8(bytes) {
101        Ok(s) => s
102            .chars()
103            .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()),
104        Err(_) => false,
105    }
106}
107
108/// Error type for hex decoding.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum DecodeError {
111    /// Input has odd length (must be pairs of hex digits)
112    OddLength,
113    /// Invalid hexadecimal character
114    InvalidChar,
115}
116
117/// Lazy hex formatter - only formats when actually displayed.
118///
119/// This avoids allocation when logging at disabled levels.
120///
121/// # Examples
122///
123/// ```
124/// use async_snmp::format::hex::Bytes;
125///
126/// let data = [0xde, 0xad, 0xbe, 0xef];
127/// let formatted = format!("{}", Bytes(&data));
128/// assert_eq!(formatted, "deadbeef");
129/// ```
130pub struct Bytes<'a>(pub &'a [u8]);
131
132impl fmt::Debug for Bytes<'_> {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        for &b in self.0 {
135            f.write_char(HEX_TABLE[(b >> 4) as usize] as char)?;
136            f.write_char(HEX_TABLE[(b & 0x0f) as usize] as char)?;
137        }
138        Ok(())
139    }
140}
141
142impl fmt::Display for Bytes<'_> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        fmt::Debug::fmt(self, f)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_bytes_display() {
154        let data = [0xde, 0xad, 0xbe, 0xef];
155        let hex = Bytes(&data);
156        assert_eq!(format!("{hex}"), "deadbeef");
157    }
158
159    #[test]
160    fn test_bytes_debug() {
161        let data = [0x00, 0xff, 0x42];
162        let hex = Bytes(&data);
163        assert_eq!(format!("{hex:?}"), "00ff42");
164    }
165
166    #[test]
167    fn test_bytes_empty() {
168        let data: [u8; 0] = [];
169        let hex = Bytes(&data);
170        assert_eq!(format!("{hex}"), "");
171    }
172
173    #[test]
174    fn test_encode_basic() {
175        assert_eq!(encode(b"Hello world!"), "48656c6c6f20776f726c6421");
176        assert_eq!(encode(&[0x01, 0x02, 0x03, 0x0f, 0x10]), "0102030f10");
177    }
178
179    #[test]
180    fn test_encode_empty() {
181        assert_eq!(encode(&[]), "");
182    }
183
184    #[test]
185    fn test_encode_all_bytes() {
186        assert_eq!(encode(&[0x00]), "00");
187        assert_eq!(encode(&[0xff]), "ff");
188        assert_eq!(encode(&[0x00, 0xff]), "00ff");
189    }
190
191    #[test]
192    fn test_decode_basic() {
193        assert_eq!(decode("48656c6c6f20776f726c6421").unwrap(), b"Hello world!");
194        assert_eq!(
195            decode("0102030f10").unwrap(),
196            vec![0x01, 0x02, 0x03, 0x0f, 0x10]
197        );
198    }
199
200    #[test]
201    fn test_decode_empty() {
202        assert_eq!(decode("").unwrap(), Vec::<u8>::new());
203    }
204
205    #[test]
206    fn test_decode_mixed_case() {
207        assert_eq!(decode("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
208        assert_eq!(decode("DEADBEEF").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
209        assert_eq!(decode("DeAdBeEf").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
210    }
211
212    #[test]
213    fn test_decode_odd_length_error() {
214        assert_eq!(decode("1"), Err(DecodeError::OddLength));
215        assert_eq!(decode("123"), Err(DecodeError::OddLength));
216        assert_eq!(decode("12345"), Err(DecodeError::OddLength));
217    }
218
219    #[test]
220    fn test_decode_invalid_char_error() {
221        assert_eq!(decode("gg"), Err(DecodeError::InvalidChar));
222        assert_eq!(decode("0g"), Err(DecodeError::InvalidChar));
223        assert_eq!(decode("g0"), Err(DecodeError::InvalidChar));
224        assert_eq!(decode("xx"), Err(DecodeError::InvalidChar));
225        assert_eq!(decode("  "), Err(DecodeError::InvalidChar));
226    }
227
228    #[test]
229    fn test_roundtrip() {
230        let original = vec![
231            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
232            0xee, 0xff,
233        ];
234        let encoded = encode(&original);
235        let decoded = decode(&encoded).unwrap();
236        assert_eq!(original, decoded);
237    }
238
239    #[test]
240    fn test_decode_relaxed_separators() {
241        assert_eq!(
242            decode_relaxed("de:ad:be:ef").unwrap(),
243            vec![0xde, 0xad, 0xbe, 0xef]
244        );
245        assert_eq!(
246            decode_relaxed("de-ad-be-ef").unwrap(),
247            vec![0xde, 0xad, 0xbe, 0xef]
248        );
249        assert_eq!(
250            decode_relaxed("de ad be ef").unwrap(),
251            vec![0xde, 0xad, 0xbe, 0xef]
252        );
253        assert_eq!(
254            decode_relaxed("deadbeef").unwrap(),
255            vec![0xde, 0xad, 0xbe, 0xef]
256        );
257    }
258
259    #[test]
260    fn test_decode_relaxed_errors() {
261        // One hex digit after stripping non-hex chars
262        assert!(decode_relaxed("a").is_err());
263        // Three hex digits - odd after stripping
264        assert!(decode_relaxed("a:b:c").is_err());
265    }
266
267    #[test]
268    fn test_is_printable() {
269        assert!(is_printable(b"Hello World"));
270        assert!(is_printable(b"Line 1\nLine 2"));
271        assert!(is_printable(b""));
272        assert!(!is_printable(&[0x00, 0x01, 0x02]));
273        assert!(!is_printable(&[0x80, 0x81]));
274    }
275
276    #[test]
277    fn test_write_to() {
278        let mut out = String::from("prefix-");
279        write_to(&mut out, &[0xde, 0xad]);
280        assert_eq!(out, "prefix-dead");
281    }
282}