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}