Skip to main content

hex_display/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4use core::fmt::{Debug, Display, LowerHex, UpperHex};
5
6#[cfg(any(feature = "std", test))]
7extern crate std;
8
9#[cfg(feature = "alloc")]
10extern crate alloc;
11
12/// An extension trait that allows for more easily constructing [`Hex`] values
13///
14/// ```
15/// use hex_display::HexDisplayExt;
16/// assert_eq!(
17///     format!("{}", [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].hex()),
18///     "0123456789abcdef"
19/// );
20/// assert_eq!(
21///     format!("{:X}", [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].hex()),
22///     "0123456789ABCDEF"
23/// );
24/// ```
25pub trait HexDisplayExt {
26    /// Display as a hexdump
27    fn hex(&self) -> Hex<'_>;
28
29    /// Convert to a upper-case hex string
30    ///
31    /// Only present when built with `alloc` support.
32    #[cfg(feature = "alloc")]
33    fn upper_hex_string(&self) -> alloc::string::String {
34        alloc::format!("{:X}", self.hex())
35    }
36
37    /// Convert to a lower-case hex string
38    ///
39    /// Only present when built with `alloc` support.
40    #[cfg(feature = "alloc")]
41    fn hex_string(&self) -> alloc::string::String {
42        use alloc::string::ToString;
43        self.hex().to_string()
44    }
45}
46
47impl HexDisplayExt for [u8] {
48    fn hex(&self) -> Hex<'_> {
49        Hex(self)
50    }
51}
52
53impl<const N: usize> HexDisplayExt for [u8; N] {
54    fn hex(&self) -> Hex<'_> {
55        Hex(self)
56    }
57}
58
59/// A wrapper type for `&[u8]` which implements Display by providing a hexdump
60///
61/// See [`HexDisplayExt`] for an easier method of constructing this type.
62///
63/// By default, it outputs a lower-case hexdump, but it outputs upper-case if provided with `{:X}`
64/// formatting option.
65///
66/// ```
67/// use hex_display::Hex;
68///
69/// assert_eq!(
70///     format!("{}", Hex(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])),
71///     "0123456789abcdef"
72/// );
73/// assert_eq!(
74///     format!("{:?}", Hex(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])),
75///     "0123456789abcdef"
76/// );
77/// assert_eq!(
78///     format!("{:X}", Hex(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])),
79///     "0123456789ABCDEF"
80/// );
81/// ```
82pub struct Hex<'a>(
83    /// The bytes to be converted into a hexdump
84    pub &'a [u8],
85);
86
87/// Lookup table indexed by nibble for lowercase hex output.
88const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
89/// Uppercase counterpart to [`LOWER_HEX`].
90const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
91
92/// Format `bytes` as hex into `f` using the supplied nibble lookup table.
93///
94/// Bytes are encoded into a stack buffer in chunks so we make one
95/// [`core::fmt::Write::write_str`] call per chunk instead of one formatting
96/// call per byte. The chunk size is chosen to comfortably fit on the stack
97/// while amortizing the formatter's per-call overhead.
98fn write_hex(
99    f: &mut core::fmt::Formatter<'_>,
100    bytes: &[u8],
101    table: &[u8; 16],
102) -> core::fmt::Result {
103    /// Bytes per chunk; produces `CHUNK * 2` ASCII hex characters per write.
104    const CHUNK: usize = 32;
105    let mut buf = [0u8; CHUNK * 2];
106    for chunk in bytes.chunks(CHUNK) {
107        for (i, &byte) in chunk.iter().enumerate() {
108            buf[i * 2] = table[(byte >> 4) as usize];
109            buf[i * 2 + 1] = table[(byte & 0x0f) as usize];
110        }
111        // The lookup table should be valid ascii, so this shouldn't panic, but is kept to
112        // safeguard against future bugs.
113        //
114        // Switching to `unsafe` was profiled and produces ~20% speedup, which was deemed to be not
115        // enough speedup to be worth introducing new `unsafe`.
116        let s = core::str::from_utf8(&buf[..chunk.len() * 2])
117            .expect("hex lookup table only emits ASCII");
118        f.write_str(s)?;
119    }
120    Ok(())
121}
122
123impl UpperHex for Hex<'_> {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        write_hex(f, self.0, UPPER_HEX)
126    }
127}
128impl LowerHex for Hex<'_> {
129    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
130        write_hex(f, self.0, LOWER_HEX)
131    }
132}
133impl Debug for Hex<'_> {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        write_hex(f, self.0, LOWER_HEX)
136    }
137}
138impl Display for Hex<'_> {
139    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
140        write_hex(f, self.0, LOWER_HEX)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use std::format;
147
148    use super::*;
149
150    #[test]
151    fn test_all_bytes() {
152        for byte in 0..=0xff {
153            assert_eq!(format!("{:02x}", byte), format!("{}", Hex(&[byte])));
154            assert_eq!(format!("{:02X}", byte), format!("{:X}", Hex(&[byte])));
155        }
156    }
157
158    #[test]
159    fn test_all_byte_pairs() {
160        for (a, b) in (0..=0xff).zip(0..=0xff) {
161            assert_eq!(format!("{:02x}{:02x}", a, b), format!("{}", Hex(&[a, b])));
162            assert_eq!(format!("{:02X}{:02X}", a, b), format!("{:X}", Hex(&[a, b])));
163        }
164    }
165}