hex_fmt/
lib.rs

1//! # Formatting and shortening byte slices as hexadecimal strings
2//!
3//! This crate provides wrappers for byte slices and lists of byte slices that implement the
4//! standard formatting traits and print the bytes as a hexadecimal string. It respects the
5//! alignment, width and precision parameters and applies padding and shortening.
6//!
7//! ```
8//! # use hex_fmt::{HexFmt, HexList};
9//! let bytes: &[u8] = &[0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f];
10//!
11//! assert_eq!("0a1b2c3d4e5f", &format!("{}", HexFmt(bytes)));
12//!
13//! // By default the full slice is printed. Change the width to apply padding or shortening.
14//! assert_eq!("0a..5f", &format!("{:6}", HexFmt(bytes)));
15//! assert_eq!("0a1b2c3d4e5f", &format!("{:12}", HexFmt(bytes)));
16//! assert_eq!("  0a1b2c3d4e5f  ", &format!("{:16}", HexFmt(bytes)));
17//!
18//! // The default alignment is centered. Use `<` or `>` to align left or right.
19//! assert_eq!("0a1b..", &format!("{:<6}", HexFmt(bytes)));
20//! assert_eq!("0a1b2c3d4e5f    ", &format!("{:<16}", HexFmt(bytes)));
21//! assert_eq!("..4e5f", &format!("{:>6}", HexFmt(bytes)));
22//! assert_eq!("    0a1b2c3d4e5f", &format!("{:>16}", HexFmt(bytes)));
23//!
24//! // Use e.g. `4.8` to set the minimum width to 4 and the maximum to 8.
25//! assert_eq!(" 12 ", &format!("{:4.8}", HexFmt([0x12])));
26//! assert_eq!("123456", &format!("{:4.8}", HexFmt([0x12, 0x34, 0x56])));
27//! assert_eq!("123..89a", &format!("{:4.8}", HexFmt([0x12, 0x34, 0x56, 0x78, 0x9a])));
28//!
29//! // If you prefer uppercase, use `X`.
30//! assert_eq!("0A1B2C3D4E5F", &format!("{:X}", HexFmt(bytes)));
31//!
32//! // All of the above can be combined.
33//! assert_eq!("0A1B2C..", &format!("{:<4.8X}", HexFmt(bytes)));
34//!
35//! // With `HexList`, the parameters are applied to each entry.
36//! let list = &[[0x0a; 3], [0x1b; 3], [0x2c; 3]];
37//! assert_eq!("[0A.., 1B.., 2C..]", &format!("{:<4X}", HexList(list)));
38//! ```
39
40#![cfg_attr(not(test), no_std)]
41
42use core::fmt::{Alignment, Debug, Display, Formatter, LowerHex, Result, UpperHex, Write};
43
44const ELLIPSIS: &str = "..";
45
46/// Wrapper for a byte array, whose `Debug`, `Display` and `LowerHex` implementations output
47/// shortened hexadecimal strings.
48pub struct HexFmt<T>(pub T);
49
50impl<T: AsRef<[u8]>> Debug for HexFmt<T> {
51    #[inline]
52    fn fmt(&self, f: &mut Formatter) -> Result {
53        LowerHex::fmt(self, f)
54    }
55}
56
57impl<T: AsRef<[u8]>> Display for HexFmt<T> {
58    #[inline]
59    fn fmt(&self, f: &mut Formatter) -> Result {
60        LowerHex::fmt(self, f)
61    }
62}
63
64impl<T: AsRef<[u8]>> LowerHex for HexFmt<T> {
65    #[inline]
66    fn fmt(&self, f: &mut Formatter) -> Result {
67        Lowercase::fmt(self.0.as_ref(), f)
68    }
69}
70
71impl<T: AsRef<[u8]>> UpperHex for HexFmt<T> {
72    #[inline]
73    fn fmt(&self, f: &mut Formatter) -> Result {
74        Uppercase::fmt(self.0.as_ref(), f)
75    }
76}
77
78/// Wrapper for a list of byte arrays, whose `Debug`, `Display` and `LowerHex` implementations
79/// output shortened hexadecimal strings.
80pub struct HexList<T>(pub T);
81
82impl<T> Debug for HexList<T>
83where
84    T: Clone + IntoIterator,
85    T::Item: AsRef<[u8]>,
86{
87    #[inline]
88    fn fmt(&self, f: &mut Formatter) -> Result {
89        LowerHex::fmt(self, f)
90    }
91}
92
93impl<T> Display for HexList<T>
94where
95    T: Clone + IntoIterator,
96    T::Item: AsRef<[u8]>,
97{
98    #[inline]
99    fn fmt(&self, f: &mut Formatter) -> Result {
100        LowerHex::fmt(self, f)
101    }
102}
103
104impl<T> LowerHex for HexList<T>
105where
106    T: Clone + IntoIterator,
107    T::Item: AsRef<[u8]>,
108{
109    #[inline]
110    fn fmt(&self, f: &mut Formatter) -> Result {
111        let entries = self.0.clone().into_iter().map(HexFmt);
112        f.debug_list().entries(entries).finish()
113    }
114}
115
116impl<T> UpperHex for HexList<T>
117where
118    T: Clone + IntoIterator,
119    T::Item: AsRef<[u8]>,
120{
121    #[inline]
122    fn fmt(&self, f: &mut Formatter) -> Result {
123        let mut iter = self.0.clone().into_iter();
124        write!(f, "[")?;
125        if let Some(item) = iter.next() {
126            UpperHex::fmt(&HexFmt(item), f)?;
127        }
128        for item in iter {
129            write!(f, ", ")?;
130            UpperHex::fmt(&HexFmt(item), f)?;
131        }
132        write!(f, "]")
133    }
134}
135
136trait Case {
137    fn fmt_byte(f: &mut Formatter, byte: u8) -> Result;
138    fn fmt_digit(f: &mut Formatter, digit: u8) -> Result;
139
140    #[inline]
141    fn fmt(bytes: &[u8], f: &mut Formatter) -> Result {
142        let min_width = f.width().unwrap_or(0);
143        let max_width = f
144            .precision()
145            .or_else(|| f.width())
146            .unwrap_or_else(usize::max_value);
147        let align = f.align().unwrap_or(Alignment::Center);
148
149        // If the array is short enough, don't shorten it.
150        if 2 * bytes.len() <= max_width {
151            let fill = f.fill();
152            let missing = min_width.saturating_sub(2 * bytes.len());
153            let (left, right) = match align {
154                Alignment::Left => (0, missing),
155                Alignment::Right => (missing, 0),
156                Alignment::Center => (missing / 2, missing - missing / 2),
157            };
158            for _ in 0..left {
159                f.write_char(fill)?;
160            }
161            for byte in bytes {
162                Self::fmt_byte(f, *byte)?;
163            }
164            for _ in 0..right {
165                f.write_char(fill)?;
166            }
167            return Ok(());
168        }
169
170        // If the bytes don't fit and the ellipsis fills the maximum width, print only that.
171        if max_width <= ELLIPSIS.len() {
172            return write!(f, "{:.*}", max_width, ELLIPSIS);
173        }
174
175        // Compute the number of hex digits to display left and right of the ellipsis.
176        let digits = max_width.saturating_sub(ELLIPSIS.len());
177        let (left, right) = match align {
178            Alignment::Left => (digits, 0),
179            Alignment::Right => (0, digits),
180            Alignment::Center => (digits - digits / 2, digits / 2),
181        };
182
183        // Print the bytes on the left.
184        for byte in &bytes[..(left / 2)] {
185            Self::fmt_byte(f, *byte)?;
186        }
187        // If odd, print only the first hex digit of the next byte.
188        if left & 1 == 1 {
189            Self::fmt_digit(f, bytes[left / 2] >> 4)?;
190        }
191
192        // Print the ellipsis.
193        f.write_str(ELLIPSIS)?;
194
195        // If `right` is odd, print the second hex digit of a byte.
196        if right & 1 == 1 {
197            Self::fmt_digit(f, bytes[(bytes.len() - right / 2 - 1)] & 0x0f)?;
198        }
199        // Print the remaining bytes on the right.
200        for byte in &bytes[(bytes.len() - right / 2)..] {
201            Self::fmt_byte(f, *byte)?;
202        }
203        Ok(())
204    }
205}
206
207#[derive(Clone, Copy)]
208struct Uppercase;
209
210impl Case for Uppercase {
211    #[inline]
212    fn fmt_byte(f: &mut Formatter, byte: u8) -> Result {
213        write!(f, "{:02X}", byte)
214    }
215
216    #[inline]
217    fn fmt_digit(f: &mut Formatter, digit: u8) -> Result {
218        write!(f, "{:1X}", digit)
219    }
220}
221
222#[derive(Clone, Copy)]
223struct Lowercase;
224
225impl Case for Lowercase {
226    #[inline]
227    fn fmt_byte(f: &mut Formatter, byte: u8) -> Result {
228        write!(f, "{:02x}", byte)
229    }
230
231    #[inline]
232    fn fmt_digit(f: &mut Formatter, digit: u8) -> Result {
233        write!(f, "{:1x}", digit)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::HexFmt;
240
241    #[test]
242    fn test_fmt() {
243        assert_eq!("", &format!("{:.0}", HexFmt(&[0x01])));
244        assert_eq!(".", &format!("{:.1}", HexFmt(&[0x01])));
245        assert_eq!("01", &format!("{:.2}", HexFmt(&[0x01])));
246        assert_eq!("..", &format!("{:.2}", HexFmt(&[0x01, 0x23])));
247    }
248}