Skip to main content

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.