1use thiserror::Error;
19
20pub(crate) const ENCODE_PAIR: [[u8; 2]; 256] = {
21 const NIBBLE: [u8; 16] = *b"0123456789abcdef";
22 let mut table = [[0u8; 2]; 256];
23 let mut i = 0u16;
24 while i < 256 {
25 table[i as usize] = [NIBBLE[(i >> 4) as usize], NIBBLE[(i & 0x0f) as usize]];
26 i += 1;
27 }
28 table
29};
30
31const DECODE_NIBBLE: [u8; 256] = {
33 let mut table = [0xFFu8; 256];
34 let mut i = 0u8;
35 while i < 10 {
36 table[(b'0' + i) as usize] = i;
37 i += 1;
38 }
39 i = 0;
40 while i < 6 {
41 table[(b'a' + i) as usize] = 10 + i;
42 table[(b'A' + i) as usize] = 10 + i;
43 i += 1;
44 }
45 table
46};
47
48#[must_use]
55pub fn encode(data: impl AsRef<[u8]>) -> String {
56 let bytes = data.as_ref();
57 let mut buf = Vec::with_capacity(bytes.len() * 2);
58 for &b in bytes {
59 buf.extend_from_slice(&ENCODE_PAIR[b as usize]);
60 }
61 String::from_utf8(buf).unwrap()
62}
63
64#[must_use]
71pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
72 let bytes = data.as_ref();
73 let mut buf = Vec::with_capacity(2 + bytes.len() * 2);
74 buf.extend_from_slice(b"0x");
75 for &b in bytes {
76 buf.extend_from_slice(&ENCODE_PAIR[b as usize]);
77 }
78 String::from_utf8(buf).unwrap()
79}
80
81pub fn decode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
87 let hex = data.as_ref();
88 if hex.len() % 2 != 0 {
89 return Err(DecodeError::OddLength);
90 }
91 let mut out = Vec::with_capacity(hex.len() / 2);
92 for pair in hex.as_chunks::<2>().0 {
93 let hi = DECODE_NIBBLE[pair[0] as usize];
94 let lo = DECODE_NIBBLE[pair[1] as usize];
95 if (hi | lo) & 0xF0 != 0 {
96 return Err(if hi == 0xFF {
97 DecodeError::InvalidChar(pair[0])
98 } else {
99 DecodeError::InvalidChar(pair[1])
100 });
101 }
102 out.push((hi << 4) | lo);
103 }
104 Ok(out)
105}
106
107pub fn decode_array<const N: usize>(data: impl AsRef<[u8]>) -> Result<[u8; N], DecodeError> {
114 let hex = data.as_ref();
115 if hex.len() != N * 2 {
116 return Err(DecodeError::LengthMismatch {
117 expected: N * 2,
118 actual: hex.len(),
119 });
120 }
121 let mut out = [0u8; N];
122
123 for (i, pair) in hex.as_chunks::<2>().0.iter().enumerate() {
124 let hi = DECODE_NIBBLE[pair[0] as usize];
125 let lo = DECODE_NIBBLE[pair[1] as usize];
126 if (hi | lo) & 0xF0 != 0 {
127 return Err(if hi == 0xFF {
128 DecodeError::InvalidChar(pair[0])
129 } else {
130 DecodeError::InvalidChar(pair[1])
131 });
132 }
133 out[i] = (hi << 4) | lo;
134 }
135 Ok(out)
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Error)]
140pub enum DecodeError {
141 #[error("odd number of hex characters")]
143 OddLength,
144 #[error("invalid hex character: {0:#04x}")]
146 InvalidChar(u8),
147 #[error("expected {expected} hex characters, was {actual}")]
149 LengthMismatch {
150 expected: usize,
152 actual: usize,
154 },
155}
156
157#[cfg(test)]
158mod tests {
159 use rstest::rstest;
160
161 use super::*;
162
163 #[rstest]
164 #[case(b"", "")]
165 #[case(b"\x00", "00")]
166 #[case(b"\xff", "ff")]
167 #[case(b"\xde\xad\xbe\xef", "deadbeef")]
168 #[case(b"hello", "68656c6c6f")]
169 fn test_encode(#[case] input: &[u8], #[case] expected: &str) {
170 assert_eq!(encode(input), expected);
171 }
172
173 #[rstest]
174 #[case("", b"")]
175 #[case("00", b"\x00")]
176 #[case("ff", b"\xff")]
177 #[case("FF", b"\xff")]
178 #[case("deadBEEF", b"\xde\xad\xbe\xef")]
179 #[case("68656c6c6f", b"hello")]
180 fn test_decode(#[case] input: &str, #[case] expected: &[u8]) {
181 assert_eq!(decode(input).unwrap(), expected);
182 }
183
184 #[rstest]
185 fn test_decode_odd_length() {
186 assert_eq!(decode("abc"), Err(DecodeError::OddLength));
187 }
188
189 #[rstest]
190 #[case("zz", DecodeError::InvalidChar(b'z'))]
191 #[case("z0", DecodeError::InvalidChar(b'z'))]
192 #[case("0z", DecodeError::InvalidChar(b'z'))]
193 fn test_decode_invalid_char(#[case] input: &str, #[case] expected: DecodeError) {
194 assert_eq!(decode(input), Err(expected));
195 }
196
197 #[rstest]
198 #[case(b"", "0x")]
199 #[case(b"\xde\xad", "0xdead")]
200 #[case(b"hello", "0x68656c6c6f")]
201 fn test_encode_prefixed(#[case] input: &[u8], #[case] expected: &str) {
202 assert_eq!(encode_prefixed(input), expected);
203 }
204
205 #[rstest]
206 fn test_decode_array() {
207 let result: [u8; 4] = decode_array("deadbeef").unwrap();
208 assert_eq!(result, [0xde, 0xad, 0xbe, 0xef]);
209 }
210
211 #[rstest]
212 fn test_decode_array_invalid_char() {
213 assert_eq!(
214 decode_array::<2>("xxff"),
215 Err(DecodeError::InvalidChar(b'x'))
216 );
217 }
218
219 #[rstest]
220 fn test_decode_array_length_mismatch() {
221 let result = decode_array::<4>("aabb");
222 assert_eq!(
223 result,
224 Err(DecodeError::LengthMismatch {
225 expected: 8,
226 actual: 4
227 })
228 );
229 }
230
231 #[rstest]
232 #[case(DecodeError::OddLength, "odd number of hex characters")]
233 #[case(DecodeError::InvalidChar(b'z'), "invalid hex character: 0x7a")]
234 #[case(
235 DecodeError::LengthMismatch { expected: 8, actual: 4 },
236 "expected 8 hex characters, was 4"
237 )]
238 fn test_decode_error_display(#[case] error: DecodeError, #[case] expected: &str) {
239 assert_eq!(error.to_string(), expected);
240 }
241
242 #[rstest]
243 fn test_roundtrip() {
244 let data = b"The quick brown fox jumps over the lazy dog";
245 assert_eq!(decode(encode(data)).unwrap(), data);
246 }
247}