1use std::fmt::Display;
19
20const 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.chunks_exact(2) {
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.chunks_exact(2).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)]
140pub enum DecodeError {
141 OddLength,
143 InvalidChar(u8),
145 LengthMismatch {
147 expected: usize,
149 actual: usize,
151 },
152}
153
154impl Display for DecodeError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 Self::OddLength => f.write_str("odd number of hex characters"),
158 Self::InvalidChar(b) => write!(f, "invalid hex character: {b:#04x}"),
159 Self::LengthMismatch { expected, actual } => {
160 write!(f, "expected {expected} hex characters, was {actual}")
161 }
162 }
163 }
164}
165
166impl std::error::Error for DecodeError {}
167
168#[cfg(test)]
169mod tests {
170 use rstest::rstest;
171
172 use super::*;
173
174 #[rstest]
175 #[case(b"", "")]
176 #[case(b"\x00", "00")]
177 #[case(b"\xff", "ff")]
178 #[case(b"\xde\xad\xbe\xef", "deadbeef")]
179 #[case(b"hello", "68656c6c6f")]
180 fn test_encode(#[case] input: &[u8], #[case] expected: &str) {
181 assert_eq!(encode(input), expected);
182 }
183
184 #[rstest]
185 #[case("", b"")]
186 #[case("00", b"\x00")]
187 #[case("ff", b"\xff")]
188 #[case("FF", b"\xff")]
189 #[case("deadBEEF", b"\xde\xad\xbe\xef")]
190 #[case("68656c6c6f", b"hello")]
191 fn test_decode(#[case] input: &str, #[case] expected: &[u8]) {
192 assert_eq!(decode(input).unwrap(), expected);
193 }
194
195 #[rstest]
196 fn test_decode_odd_length() {
197 assert_eq!(decode("abc"), Err(DecodeError::OddLength));
198 }
199
200 #[rstest]
201 #[case("zz", DecodeError::InvalidChar(b'z'))]
202 #[case("z0", DecodeError::InvalidChar(b'z'))]
203 #[case("0z", DecodeError::InvalidChar(b'z'))]
204 fn test_decode_invalid_char(#[case] input: &str, #[case] expected: DecodeError) {
205 assert_eq!(decode(input), Err(expected));
206 }
207
208 #[rstest]
209 #[case(b"", "0x")]
210 #[case(b"\xde\xad", "0xdead")]
211 #[case(b"hello", "0x68656c6c6f")]
212 fn test_encode_prefixed(#[case] input: &[u8], #[case] expected: &str) {
213 assert_eq!(encode_prefixed(input), expected);
214 }
215
216 #[rstest]
217 fn test_decode_array() {
218 let result: [u8; 4] = decode_array("deadbeef").unwrap();
219 assert_eq!(result, [0xde, 0xad, 0xbe, 0xef]);
220 }
221
222 #[rstest]
223 fn test_decode_array_invalid_char() {
224 assert_eq!(
225 decode_array::<2>("xxff"),
226 Err(DecodeError::InvalidChar(b'x'))
227 );
228 }
229
230 #[rstest]
231 fn test_decode_array_length_mismatch() {
232 let result = decode_array::<4>("aabb");
233 assert_eq!(
234 result,
235 Err(DecodeError::LengthMismatch {
236 expected: 8,
237 actual: 4
238 })
239 );
240 }
241
242 #[rstest]
243 #[case(DecodeError::OddLength, "odd number of hex characters")]
244 #[case(DecodeError::InvalidChar(b'z'), "invalid hex character: 0x7a")]
245 #[case(
246 DecodeError::LengthMismatch { expected: 8, actual: 4 },
247 "expected 8 hex characters, was 4"
248 )]
249 fn test_decode_error_display(#[case] error: DecodeError, #[case] expected: &str) {
250 assert_eq!(error.to_string(), expected);
251 }
252
253 #[rstest]
254 fn test_roundtrip() {
255 let data = b"The quick brown fox jumps over the lazy dog";
256 assert_eq!(decode(encode(data)).unwrap(), data);
257 }
258}