1use snafu::{Snafu, ensure};
8
9#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
11pub enum HexError {
12 #[snafu(display("hex digest must be 64 characters; got {length}"))]
14 InvalidLength {
15 length: usize,
17 },
18
19 #[snafu(display("hex digest contains invalid character {byte:#04x} at offset {offset}"))]
21 InvalidByte {
22 offset: usize,
24 byte: u8,
26 },
27}
28
29#[must_use]
31pub fn encode_32(bytes: &[u8; 32]) -> String {
32 let mut out = String::with_capacity(64);
33 for b in bytes {
34 out.push(nibble(b >> 4));
35 out.push(nibble(b & 0x0F));
36 }
37 out
38}
39
40pub fn decode_32(s: &str) -> Result<[u8; 32], HexError> {
49 let bytes = s.as_bytes();
50 ensure!(
51 bytes.len() == 64,
52 InvalidLengthSnafu {
53 length: bytes.len()
54 }
55 );
56 let mut out = [0u8; 32];
57 for (i, chunk) in bytes.chunks_exact(2).enumerate() {
58 let hi = decode_nibble(chunk[0], i * 2)?;
59 let lo = decode_nibble(chunk[1], i * 2 + 1)?;
60 out[i] = (hi << 4) | lo;
61 }
62 Ok(out)
63}
64
65const fn nibble(n: u8) -> char {
66 match n {
67 0 => '0',
68 1 => '1',
69 2 => '2',
70 3 => '3',
71 4 => '4',
72 5 => '5',
73 6 => '6',
74 7 => '7',
75 8 => '8',
76 9 => '9',
77 10 => 'a',
78 11 => 'b',
79 12 => 'c',
80 13 => 'd',
81 14 => 'e',
82 _ => 'f',
83 }
84}
85
86fn decode_nibble(byte: u8, offset: usize) -> Result<u8, HexError> {
87 match byte {
88 b'0'..=b'9' => Ok(byte - b'0'),
89 b'a'..=b'f' => Ok(byte - b'a' + 10),
90 b'A'..=b'F' => Ok(byte - b'A' + 10),
91 _ => Err(HexError::InvalidByte { offset, byte }),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use crate::hex::{HexError, decode_32, encode_32};
98
99 #[test]
100 fn encodes_all_zeros() {
101 let bytes = [0u8; 32];
102 let hex = encode_32(&bytes);
103 assert_eq!(hex.len(), 64);
104 assert!(hex.chars().all(|c| c == '0'));
105 }
106
107 #[test]
108 fn encodes_all_ff() {
109 let bytes = [0xFFu8; 32];
110 let hex = encode_32(&bytes);
111 assert_eq!(hex.len(), 64);
112 assert!(hex.chars().all(|c| c == 'f'));
113 }
114
115 #[test]
116 fn round_trip_random_pattern() {
117 let mut bytes = [0u8; 32];
118 for (i, b) in bytes.iter_mut().enumerate() {
119 *b = u8::try_from((i * 7) & 0xFF).unwrap();
121 }
122 let hex = encode_32(&bytes);
123 let back = decode_32(&hex).unwrap();
124 assert_eq!(back, bytes);
125 }
126
127 #[test]
128 fn output_is_lowercase_only() {
129 let bytes = [0xAB; 32];
130 let hex = encode_32(&bytes);
131 assert!(hex.chars().all(|c| !c.is_ascii_uppercase()));
132 assert_eq!(&hex[..2], "ab");
133 }
134
135 #[test]
136 fn decode_accepts_uppercase() {
137 let lower = "ab".repeat(32);
138 let upper = "AB".repeat(32);
139 assert_eq!(decode_32(&lower).unwrap(), decode_32(&upper).unwrap());
140 }
141
142 #[test]
143 fn decode_rejects_short_input() {
144 let err = decode_32("ab").unwrap_err();
145 assert!(matches!(err, HexError::InvalidLength { length: 2 }));
146 }
147
148 #[test]
149 fn decode_rejects_long_input() {
150 let s = "a".repeat(100);
151 let err = decode_32(&s).unwrap_err();
152 assert!(matches!(err, HexError::InvalidLength { length: 100 }));
153 }
154
155 #[test]
156 fn decode_rejects_non_hex_character() {
157 let mut s = "a".repeat(64);
159 s.replace_range(30..31, "z");
160 let err = decode_32(&s).unwrap_err();
161 match err {
162 HexError::InvalidByte { offset, byte } => {
163 assert_eq!(offset, 30);
164 assert_eq!(byte, b'z');
165 }
166 HexError::InvalidLength { .. } => {
167 panic!("expected InvalidByte, got {err:?}")
168 }
169 }
170 }
171
172 #[test]
173 fn encode_matches_known_vector() {
174 let mut bytes = [0u8; 32];
176 bytes[0] = 0xaf;
177 bytes[1] = 0x13;
178 bytes[2] = 0x49;
179 bytes[3] = 0xb9;
180 let hex = encode_32(&bytes);
181 assert_eq!(&hex[..8], "af1349b9");
182 }
183}