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