1use std::fmt;
27
28#[derive(Debug, PartialEq, Eq)]
30pub enum HexError {
31 OddLength,
33 InvalidChar(char),
35}
36
37impl fmt::Display for HexError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 HexError::OddLength => f.write_str("odd number of hex digits"),
41 HexError::InvalidChar(c) => write!(f, "invalid hex character: {c:?}"),
42 }
43 }
44}
45
46impl std::error::Error for HexError {}
47
48pub fn encode(bytes: &[u8]) -> String {
59 bytes
60 .iter()
61 .flat_map(|b| nibble_to_hex(b >> 4, false).chain(nibble_to_hex(b & 0xf, false)))
62 .collect()
63}
64
65pub fn encode_upper(bytes: &[u8]) -> String {
75 bytes
76 .iter()
77 .flat_map(|b| nibble_to_hex(b >> 4, true).chain(nibble_to_hex(b & 0xf, true)))
78 .collect()
79}
80
81fn nibble_to_hex(nibble: u8, upper: bool) -> std::array::IntoIter<char, 1> {
82 let ch = match nibble {
83 0..=9 => b'0' + nibble,
84 _ if upper => b'A' + nibble - 10,
85 _ => b'a' + nibble - 10,
86 };
87 [ch as char].into_iter()
88}
89
90pub fn decode(hex: &str) -> Result<Vec<u8>, HexError> {
111 let hex = hex
112 .strip_prefix("0x")
113 .or_else(|| hex.strip_prefix("0X"))
114 .unwrap_or(hex);
115
116 if !hex.len().is_multiple_of(2) {
117 return Err(HexError::OddLength);
118 }
119
120 hex.as_bytes()
121 .chunks(2)
122 .map(|pair| {
123 let hi = from_hex_char(pair[0] as char)?;
124 let lo = from_hex_char(pair[1] as char)?;
125 Ok((hi << 4) | lo)
126 })
127 .collect()
128}
129
130fn from_hex_char(c: char) -> Result<u8, HexError> {
131 match c {
132 '0'..='9' => Ok(c as u8 - b'0'),
133 'a'..='f' => Ok(c as u8 - b'a' + 10),
134 'A'..='F' => Ok(c as u8 - b'A' + 10),
135 _ => Err(HexError::InvalidChar(c)),
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn encode_empty() {
145 assert_eq!(encode(b""), "");
146 }
147
148 #[test]
149 fn encode_single_byte() {
150 assert_eq!(encode(b"\xff"), "ff");
151 assert_eq!(encode(b"\x00"), "00");
152 }
153
154 #[test]
155 fn encode_lowercase() {
156 assert_eq!(encode(b"\xde\xad\xbe\xef"), "deadbeef");
157 }
158
159 #[test]
160 fn encode_upper_uppercase() {
161 assert_eq!(encode_upper(b"\xde\xad\xbe\xef"), "DEADBEEF");
162 }
163
164 #[test]
165 fn encode_roundtrip() {
166 let original = b"Hello, world!\x00\xff";
167 assert_eq!(decode(&encode(original)).unwrap(), original);
168 }
169
170 #[test]
171 fn decode_empty() {
172 assert_eq!(decode("").unwrap(), b"");
173 }
174
175 #[test]
176 fn decode_lowercase() {
177 assert_eq!(decode("deadbeef").unwrap(), b"\xde\xad\xbe\xef");
178 }
179
180 #[test]
181 fn decode_uppercase() {
182 assert_eq!(decode("DEADBEEF").unwrap(), b"\xde\xad\xbe\xef");
183 }
184
185 #[test]
186 fn decode_mixed_case() {
187 assert_eq!(decode("DeAdBeEf").unwrap(), b"\xde\xad\xbe\xef");
188 }
189
190 #[test]
191 fn decode_0x_prefix() {
192 assert_eq!(decode("0xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
193 assert_eq!(decode("0Xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
194 }
195
196 #[test]
197 fn decode_odd_length_errors() {
198 assert_eq!(decode("abc").unwrap_err(), HexError::OddLength);
199 }
200
201 #[test]
202 fn decode_invalid_char_errors() {
203 assert!(matches!(
204 decode("zz").unwrap_err(),
205 HexError::InvalidChar('z')
206 ));
207 }
208
209 #[test]
210 fn encode_all_bytes() {
211 let all: Vec<u8> = (0u8..=255).collect();
212 let hex = encode(&all);
213 assert_eq!(hex.len(), 512);
214 assert_eq!(decode(&hex).unwrap(), all);
215 }
216}