1use std::fmt;
4use std::fmt::Write;
5
6const HEX_TABLE: &[u8; 16] = b"0123456789abcdef";
7
8#[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
25pub 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
35pub 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
59pub 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum DecodeError {
111 OddLength,
113 InvalidChar,
115}
116
117pub 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 assert!(decode_relaxed("a").is_err());
263 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}