commonware_formatting/hex_literal.rs
1#![doc(hidden)]
2
3//! Implementation of the [`crate::hex!`] macro.
4//!
5//! Modified from the [`hex-literal`](https://github.com/RustCrypto/utils/tree/master/hex-literal)
6//! crate to allow `0x` prefixes.
7//!
8//! Vendored from [`alloy-primitives`](https://github.com/alloy-rs/core/tree/main/crates/primitives).
9//!
10//! This module is public only so that macro expansions can access it from downstream crates.
11//! Callers must not invoke its functions directly. For accepted input, the macro maintains these
12//! invariants:
13//!
14//! - Each literal has at most one leading `0x` or `0X` prefix removed.
15//! - The decoded length is computed from the same prefix-free literals that are decoded.
16//! - The computed length equals the number of bytes represented by those literals.
17
18const fn next_hex_char(string: &[u8], mut pos: usize) -> Option<(u8, usize)> {
19 while pos < string.len() {
20 let raw_val = string[pos];
21 pos += 1;
22 let val = match raw_val {
23 b'0'..=b'9' => raw_val - 48,
24 b'A'..=b'F' => raw_val - 55,
25 b'a'..=b'f' => raw_val - 87,
26 b' ' | b'\r' | b'\n' | b'\t' => continue,
27 0..=127 => panic!("Encountered invalid ASCII character"),
28 _ => panic!("Encountered non-ASCII character"),
29 };
30 return Some((val, pos));
31 }
32 None
33}
34
35const fn next_byte(string: &[u8], pos: usize) -> Option<(u8, usize)> {
36 let (half1, pos) = match next_hex_char(string, pos) {
37 Some(v) => v,
38 None => return None,
39 };
40 let (half2, pos) = match next_hex_char(string, pos) {
41 Some(v) => v,
42 None => panic!("Odd number of hex characters"),
43 };
44 Some(((half1 << 4) + half2, pos))
45}
46
47/// Removes at most one leading `0x` or `0X` prefix from `string`.
48///
49/// All other input is returned unchanged. This function is an implementation detail and callers
50/// must not invoke it directly.
51#[doc(hidden)]
52pub const fn strip_hex_prefix(string: &[u8]) -> &[u8] {
53 if let [b'0', b'x' | b'X', rest @ ..] = string {
54 rest
55 } else {
56 string
57 }
58}
59
60/// Computes the number of bytes represented by `strings`.
61///
62/// Each string must be prefix-free and contain an even number of hexadecimal digits after spaces,
63/// tabs, carriage returns, and newlines are ignored. This function is an implementation detail and
64/// callers must not invoke it directly.
65///
66/// # Panics
67///
68/// Panics if a string contains anything other than a hexadecimal digit, space, tab, carriage
69/// return, or newline, or if it contains an odd number of hexadecimal digits.
70#[doc(hidden)]
71pub const fn len(strings: &[&[u8]]) -> usize {
72 let mut i = 0;
73 let mut len = 0;
74 while i < strings.len() {
75 let mut pos = 0;
76 while let Some((_, new_pos)) = next_byte(strings[i], pos) {
77 len += 1;
78 pos = new_pos;
79 }
80 i += 1;
81 }
82 len
83}
84
85/// Decodes `strings` into a byte array with a precomputed length.
86///
87/// `LEN` must equal [`len(strings)`](len). The [`crate::hex!`] macro guarantees this by computing
88/// `LEN` from the same prefix-free strings passed to this function. This function is an
89/// implementation detail and callers must not invoke it directly.
90///
91/// # Panics
92///
93/// Panics if a string contains anything other than a hexadecimal digit, space, tab, carriage
94/// return, or newline, if it contains an odd number of hexadecimal digits, or if `LEN` differs from
95/// the number of decoded bytes.
96#[doc(hidden)]
97pub const fn decode<const LEN: usize>(strings: &[&[u8]]) -> [u8; LEN] {
98 let mut i = 0;
99 let mut buf = [0u8; LEN];
100 let mut buf_pos = 0;
101 while i < strings.len() {
102 let mut pos = 0;
103 while let Some((byte, new_pos)) = next_byte(strings[i], pos) {
104 buf[buf_pos] = byte;
105 buf_pos += 1;
106 pos = new_pos;
107 }
108 i += 1;
109 }
110 if LEN != buf_pos {
111 panic!("Length mismatch. Please report this bug.");
112 }
113 buf
114}
115
116/// Converts string literals containing hexadecimal data into a byte array.
117///
118/// Each literal may begin with `0x` or `0X`; when present, the prefix must be its first two
119/// characters. Spaces, tabs, carriage returns, and newlines are ignored. Each literal must contain
120/// an even number of hexadecimal digits after its optional prefix and whitespace are removed.
121///
122/// The array length is computed from the same prefix-free literals that are decoded, so the output
123/// contains exactly the bytes represented by the input.
124///
125/// # Examples
126///
127/// ```
128/// use commonware_formatting::hex;
129///
130/// const BYTES: [u8; 4] = hex!("0x12 34" "0Xab cd");
131/// assert_eq!(BYTES, [0x12, 0x34, 0xab, 0xcd]);
132/// ```
133#[macro_export]
134macro_rules! hex {
135 ($($s:literal)*) => {const {
136 const STRINGS: &[&[u8]] = &[$( $crate::hex_literal::strip_hex_prefix($s.as_bytes()), )*];
137 $crate::hex_literal::decode::<{ $crate::hex_literal::len(STRINGS) }>(STRINGS)
138 }};
139}
140
141#[cfg(test)]
142mod tests {
143 #[test]
144 fn single_literal() {
145 assert_eq!(hex!("ff e4"), [0xff, 0xe4]);
146 }
147
148 #[test]
149 fn empty() {
150 let nothing: [u8; 0] = hex!();
151 let empty_literals: [u8; 0] = hex!("" "" "");
152 let expected: [u8; 0] = [];
153 assert_eq!(nothing, expected);
154 assert_eq!(empty_literals, expected);
155 }
156
157 #[test]
158 fn upper_case() {
159 assert_eq!(hex!("AE DF 04 B2"), [0xae, 0xdf, 0x04, 0xb2]);
160 assert_eq!(hex!("FF BA 8C 00 01"), [0xff, 0xba, 0x8c, 0x00, 0x01]);
161 }
162
163 #[test]
164 fn mixed_case() {
165 assert_eq!(hex!("bF dd E4 Cd"), [0xbf, 0xdd, 0xe4, 0xcd]);
166 }
167
168 #[test]
169 fn optional_prefix() {
170 assert_eq!(hex!("1a2b3c"), [0x1a, 0x2b, 0x3c]);
171 assert_eq!(hex!("0x1a2b3c"), [0x1a, 0x2b, 0x3c]);
172 assert_eq!(hex!("0X1a2b3c"), [0x1a, 0x2b, 0x3c]);
173 assert_eq!(hex!("0xa1" "b2" "0Xc3"), [0xa1, 0xb2, 0xc3]);
174 }
175
176 #[test]
177 fn strips_exactly_one_prefix() {
178 assert_eq!(super::strip_hex_prefix(b"0x12"), b"12");
179 assert_eq!(super::strip_hex_prefix(b"0X12"), b"12");
180 assert_eq!(super::strip_hex_prefix(b"0x0X12"), b"0X12");
181 assert_eq!(super::strip_hex_prefix(b"x012"), b"x012");
182 }
183
184 #[test]
185 fn computed_length_matches_decoded_bytes() {
186 const STRINGS: &[&[u8]] = &[
187 super::strip_hex_prefix(b"0x12 34"),
188 super::strip_hex_prefix(b"0Xab\ncd"),
189 ];
190 const LEN: usize = super::len(STRINGS);
191
192 assert_eq!(LEN, 4);
193 assert_eq!(super::decode::<LEN>(STRINGS), [0x12, 0x34, 0xab, 0xcd]);
194 }
195
196 #[test]
197 fn multiple_literals() {
198 assert_eq!(
199 hex!(
200 "01 dd f7 7f"
201 "ee f0 d8"
202 ),
203 [0x01, 0xdd, 0xf7, 0x7f, 0xee, 0xf0, 0xd8]
204 );
205 assert_eq!(
206 hex!(
207 "ff"
208 "e8 d0"
209 ""
210 "01 1f"
211 "ab"
212 ),
213 [0xff, 0xe8, 0xd0, 0x01, 0x1f, 0xab]
214 );
215 }
216
217 #[test]
218 fn no_spacing() {
219 assert_eq!(hex!("abf0d8bb0f14"), [0xab, 0xf0, 0xd8, 0xbb, 0x0f, 0x14]);
220 assert_eq!(
221 hex!("09FFd890cbcCd1d08F"),
222 [0x09, 0xff, 0xd8, 0x90, 0xcb, 0xcc, 0xd1, 0xd0, 0x8f]
223 );
224 }
225
226 #[test]
227 fn allows_various_spacing() {
228 // newlines
229 assert_eq!(
230 hex!(
231 "f
232 f
233 d
234 0
235 e
236
237 8
238 "
239 ),
240 [0xff, 0xd0, 0xe8]
241 );
242 // tabs
243 assert_eq!(hex!("9f d 1 f07 3 01 "), [0x9f, 0xd1, 0xf0, 0x73, 0x01]);
244 // spaces
245 assert_eq!(hex!(" e e d0 9 1 f f "), [0xee, 0xd0, 0x91, 0xff]);
246 }
247
248 #[test]
249 const fn can_use_const() {
250 const _: [u8; 4] = hex!("ff d3 01 7f");
251 }
252}
253
254// https://github.com/alloy-rs/core/blob/main/LICENSE-MIT
255//
256// Permission is hereby granted, free of charge, to any
257// person obtaining a copy of this software and associated
258// documentation files (the "Software"), to deal in the
259// Software without restriction, including without
260// limitation the rights to use, copy, modify, merge,
261// publish, distribute, sublicense, and/or sell copies of
262// the Software, and to permit persons to whom the Software
263// is furnished to do so, subject to the following
264// conditions:
265//
266// The above copyright notice and this permission notice
267// shall be included in all copies or substantial portions
268// of the Software.
269//
270// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
271// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
272// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
273// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
274// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
275// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
276// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
277// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
278// DEALINGS IN THE SOFTWARE.