Skip to main content

commonware_formatting/
lib.rs

1//! Format and parse encoded data.
2
3#![doc(
4    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
5    html_favicon_url = "https://commonware.xyz/favicon.ico"
6)]
7#![cfg_attr(not(any(feature = "std", test)), no_std)]
8
9// Declared at the crate root (rather than inside the `stability_scope!` block
10// below) so the `#[macro_export] macro_rules! hex` it contains can be
11// referenced via absolute paths within this crate.
12commonware_macros::stability_mod!(BETA, pub mod hex_literal);
13
14commonware_macros::stability_scope!(BETA {
15    extern crate alloc;
16
17    use alloc::{string::String, vec::Vec};
18    use core::fmt;
19
20    /// Converts bytes to a lowercase hexadecimal [String].
21    pub fn hex(bytes: &[u8]) -> String {
22        const_hex::encode(bytes)
23    }
24
25    /// Converts a hexadecimal string to bytes, stripping ASCII whitespace
26    /// and an optional `0x` / `0X` prefix.
27    pub fn from_hex(s: &str) -> Option<Vec<u8>> {
28        // "0x" prefix stripping is handled by `const-hex::decode`.
29        let s = s.replace(['\t', '\n', '\r', ' '], "");
30        let stripped = s
31            .strip_prefix("0X")
32            .unwrap_or(&s);
33        const_hex::decode(stripped).ok()
34    }
35
36    /// Display/Debug wrapper that renders bytes as lowercase hex without
37    /// allocating an intermediate [String].
38    ///
39    /// Use this in `Display` or `Debug` implementations to format a byte slice
40    /// directly into the output `Formatter` via `const-hex`'s stack-allocated
41    /// buffer. For owned conversion to [String], use [hex()] instead.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use commonware_formatting::Hex;
47    ///
48    /// struct Digest([u8; 32]);
49    ///
50    /// impl core::fmt::Display for Digest {
51    ///     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52    ///         write!(f, "{}", Hex(&self.0))
53    ///     }
54    /// }
55    /// ```
56    pub struct Hex<T: AsRef<[u8]>>(pub T);
57
58    impl<T: AsRef<[u8]>> From<T> for Hex<T> {
59        fn from(value: T) -> Self {
60            Self(value)
61        }
62    }
63
64    impl<T: AsRef<[u8]>> fmt::Display for Hex<T> {
65        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66            write_hex(self.0.as_ref(), f)
67        }
68    }
69
70    impl<T: AsRef<[u8]>> fmt::Debug for Hex<T> {
71        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72            write_hex(self.0.as_ref(), f)
73        }
74    }
75
76    /// Writes `bytes` to the formatter as lowercase hex without heap allocation.
77    ///
78    /// Uses a fixed-size stack buffer per chunk to avoid bounding the input
79    /// length at compile time.
80    fn write_hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        // Encode in chunks that fit in a stack buffer to support arbitrary input lengths.
82        const CHUNK: usize = 64;
83        let mut buf = [0u8; CHUNK * 2];
84        for slice in bytes.chunks(CHUNK) {
85            let out = &mut buf[..slice.len() * 2];
86            const_hex::encode_to_slice(slice, out).expect("slice fits in buffer");
87            // SAFETY: `encode_to_slice` writes only ASCII hex digits, which are valid UTF-8.
88            let s = unsafe { core::str::from_utf8_unchecked(out) };
89            f.write_str(s)?;
90        }
91        Ok(())
92    }
93});
94
95#[cfg(test)]
96mod tests {
97    use crate::{from_hex, Hex};
98
99    #[test]
100    fn test_hex_roundtrip() {
101        for (bytes, encoded) in [
102            (&[][..], ""),
103            (&[0x01][..], "01"),
104            (&[0x01, 0x02, 0x03][..], "010203"),
105        ] {
106            assert_eq!(crate::hex(bytes), encoded);
107            assert_eq!(from_hex(encoded).unwrap(), bytes.to_vec());
108        }
109    }
110
111    #[test]
112    fn test_from_hex() {
113        let expected: Vec<u8> = vec![0x01, 0x02, 0x03];
114
115        // No formatting
116        assert_eq!(from_hex("010203").unwrap(), expected);
117
118        // Whitespace
119        assert_eq!(from_hex("01 02 03").unwrap(), expected);
120
121        // 0x prefix (lowercase)
122        assert_eq!(from_hex("0x010203").unwrap(), expected);
123
124        // 0X prefix (uppercase)
125        assert_eq!(from_hex("0X010203").unwrap(), expected);
126
127        // 0x prefix + mixed whitespace (tabs, newlines, spaces, carriage returns)
128        let h = "    \n\n0x\r\n01
129                            02\t03\n";
130        assert_eq!(from_hex(h).unwrap(), expected);
131
132        // Empty string
133        assert_eq!(from_hex(""), Some(vec![]));
134
135        // Odd length
136        assert!(from_hex("0102030").is_none());
137
138        // Invalid hex character
139        assert!(from_hex("01g3").is_none());
140
141        // Invalid `+`
142        assert!(from_hex("+123").is_none());
143    }
144
145    #[test]
146    fn test_from_hex_utf8_char_boundaries() {
147        // Ensure that `from_hex` handles misaligned UTF-8 character boundaries.
148        const MISALIGNMENT_CASE: &str = "쀘\n";
149        assert!(from_hex(MISALIGNMENT_CASE).is_none());
150    }
151
152    #[test]
153    fn test_hex_newtype_display() {
154        let bytes = [0x01u8, 0x02, 0xab, 0xcd];
155        let s = format!("{}", Hex(&bytes[..]));
156        assert_eq!(s, "0102abcd");
157
158        // Owned input
159        let v = bytes.to_vec();
160        assert_eq!(format!("{}", Hex(v)), "0102abcd");
161
162        // Empty
163        assert_eq!(format!("{}", Hex::<&[u8]>(&[])), "");
164
165        // Larger than the internal CHUNK to exercise the loop
166        let big: Vec<u8> = (0..200u16).map(|i| i as u8).collect();
167        let formatted = format!("{}", Hex(&big));
168        assert_eq!(formatted, super::hex(&big));
169    }
170
171    #[test]
172    fn test_hex_newtype_debug() {
173        let bytes = [0xff, 0x00];
174        assert_eq!(format!("{:?}", Hex(&bytes[..])), "ff00");
175    }
176
177    #[test]
178    fn test_hex_newtype_from() {
179        let bytes = [0x01u8, 0x02, 0xab, 0xcd];
180
181        // From<T> for Hex<T> is callable both ways round.
182        let from_slice: Hex<&[u8]> = (&bytes[..]).into();
183        assert_eq!(format!("{from_slice}"), "0102abcd");
184        let from_owned: Hex<Vec<u8>> = bytes.to_vec().into();
185        assert_eq!(format!("{from_owned}"), "0102abcd");
186    }
187}