1use crate::sexpr::SExprError;
14
15#[must_use]
21pub fn encode(bytes: &[u8]) -> String {
22 let mut out = String::with_capacity(bytes.len() * 2);
23 for b in bytes {
24 out.push(nibble(b >> 4));
25 out.push(nibble(b & 0xF));
26 }
27 out
28}
29
30pub fn decode(s: &str) -> Result<Vec<u8>, SExprError> {
39 if s.len() % 2 != 0 {
40 return Err(SExprError::Parse("hex string must have even length".into()));
41 }
42 let mut out = Vec::with_capacity(s.len() / 2);
43 for chunk in s.as_bytes().chunks(2) {
44 let pair =
45 std::str::from_utf8(chunk).map_err(|_| SExprError::Parse("non-ascii in hex".into()))?;
46 let byte =
47 u8::from_str_radix(pair, 16).map_err(|e| SExprError::Parse(format!("bad hex: {e}")))?;
48 out.push(byte);
49 }
50 Ok(out)
51}
52
53pub(crate) fn nibble(n: u8) -> char {
56 match n {
57 0..=9 => (b'0' + n) as char,
58 10..=15 => (b'a' + (n - 10)) as char,
59 _ => unreachable!("nibble must be 0..=15"),
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn encode_empty_is_empty() {
69 assert_eq!(encode(&[]), "");
70 }
71
72 #[test]
73 fn encode_is_lowercase() {
74 assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
77 }
78
79 #[test]
80 fn encode_length_is_twice_input() {
81 for n in 0..16 {
82 let bytes: Vec<u8> = (0..n).collect();
83 assert_eq!(encode(&bytes).len(), n as usize * 2);
84 }
85 }
86
87 #[test]
88 fn decode_empty_is_empty_vec() {
89 assert!(decode("").unwrap().is_empty());
90 }
91
92 #[test]
93 fn decode_rejects_odd_length() {
94 let err = decode("abc").unwrap_err();
95 assert!(format!("{err:?}").contains("even length"));
96 }
97
98 #[test]
99 fn decode_rejects_non_ascii() {
100 let err = decode("aña").unwrap_err();
108 assert!(
109 format!("{err:?}").contains("non-ascii"),
110 "unexpected: {err:?}"
111 );
112 }
113
114 #[test]
115 fn decode_rejects_non_hex_digits() {
116 let err = decode("ag").unwrap_err();
117 assert!(format!("{err:?}").contains("bad hex"));
118 }
119
120 #[test]
121 fn decode_accepts_uppercase_mixed_case() {
122 assert_eq!(decode("DEADBEEF").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
127 assert_eq!(decode("DeadBeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
128 }
129
130 #[test]
131 fn roundtrip_every_byte_value() {
132 for b in 0u8..=255u8 {
134 let enc = encode(&[b]);
135 assert_eq!(enc.len(), 2);
136 let dec = decode(&enc).unwrap();
137 assert_eq!(dec, vec![b], "failed for byte 0x{b:02x}");
138 }
139 }
140
141 #[test]
142 fn roundtrip_larger_buffer() {
143 let original: Vec<u8> = (0..=255).chain((0..=255).rev()).collect();
144 let enc = encode(&original);
145 assert_eq!(enc.len(), original.len() * 2);
146 let dec = decode(&enc).unwrap();
147 assert_eq!(dec, original);
148 }
149
150 #[test]
151 fn nibble_boundaries() {
152 assert_eq!(nibble(0), '0');
153 assert_eq!(nibble(9), '9');
154 assert_eq!(nibble(10), 'a');
155 assert_eq!(nibble(15), 'f');
156 }
157
158 #[test]
159 #[should_panic(expected = "nibble must be 0..=15")]
160 fn nibble_panics_above_range() {
161 let _ = nibble(16);
165 }
166}