Skip to main content

faster_hex/
format.rs

1use crate::encode::encode;
2use core::fmt::{self, Alignment, Write};
3use core::mem::MaybeUninit;
4
5/// A borrowed hexadecimal view of a byte slice, available without allocation.
6///
7/// [`Display`](fmt::Display) and [`LowerHex`](fmt::LowerHex) use lowercase digits;
8/// [`UpperHex`](fmt::UpperHex) uses uppercase. Every byte produces two digits in
9/// source order, including leading zeroes. The slice is borrowed, not copied.
10/// This is a byte-sequence view: it does not interpret the input as an integer
11/// or reverse bytes according to the machine's endianness.
12///
13/// # Formatting
14///
15/// All three formatting traits use the following integer-style flags:
16///
17/// - `#` adds `0x`, also for uppercase output and an empty slice.
18/// - `+` adds a leading plus sign. Width includes the sign and prefix.
19/// - Width, fill and alignment apply to the complete value; default alignment is right.
20///   Width counts Unicode characters, so a non-ASCII fill character counts as one.
21/// - `0` pads after the sign/prefix, overriding fill and alignment.
22/// - Precision and `-` are ignored. Precision never truncates a byte sequence.
23///
24/// # Allocation and writer errors
25///
26/// Available without `alloc` or `std`. Formatting uses bounded stack storage and
27/// allocates no intermediate string; the destination writer may still allocate.
28/// For example, `format!` allocates its resulting string, while `write!` can write
29/// into existing storage.
30///
31/// An error from the writer is returned immediately and no further writes are
32/// attempted. Text already accepted by the writer remains written, including a
33/// partial write from the failing call. The number and size of writes are
34/// unspecified. To make output atomic, format into a separate buffer first.
35///
36/// # Examples
37///
38/// ```
39/// use faster_hex::Hex;
40/// let bytes = [0, 0xab, 0xcd];
41/// let hex = Hex::new(&bytes);
42/// assert_eq!(format!("{hex}"), "00abcd");
43/// assert_eq!(format!("{hex:#010X}"), "0x0000ABCD");
44/// assert_eq!(format!("{hex:.2}"), "00abcd");
45/// ```
46///
47/// Append to a text destination without creating an intermediate hex string:
48///
49/// ```
50/// use core::fmt::Write;
51/// use faster_hex::Hex;
52///
53/// let mut output = String::with_capacity(64);
54/// write!(output, "hash={:#X}", Hex::new(&[0, 0xab, 0xcd]))?;
55/// assert_eq!(output, "hash=0x00ABCD");
56/// # Ok::<(), core::fmt::Error>(())
57/// ```
58#[derive(Clone, Copy, Debug)]
59#[must_use]
60pub struct Hex<'a> {
61    bytes: &'a [u8],
62}
63
64impl<'a> Hex<'a> {
65    /// Borrows bytes for hexadecimal formatting without copying or allocating.
66    ///
67    /// The view cannot outlive `bytes`. It accepts an empty slice and is usable
68    /// in constant expressions. Creating a view does not perform any encoding.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use faster_hex::Hex;
74    ///
75    /// const ID: Hex<'static> = Hex::new(&[0, 0xab]);
76    /// assert_eq!(format!("{ID}"), "00ab");
77    /// assert_eq!(format!("{ID:#X}"), "0x00AB");
78    /// ```
79    pub const fn new(bytes: &'a [u8]) -> Self {
80        Self { bytes }
81    }
82
83    fn format(&self, f: &mut fmt::Formatter<'_>, upper: bool) -> fmt::Result {
84        // The usual hash/logging format has no field width. It needs neither
85        // encoded-length arithmetic nor alignment/padding calculations.
86        if let Some(width) = f.width() {
87            return self.format_padded(f, upper, width);
88        }
89        if f.sign_plus() {
90            f.write_str("+")?;
91        }
92        if f.alternate() {
93            f.write_str("0x")?;
94        }
95        self.write_hex(f, upper)
96    }
97
98    // Padding needs more live state; keep it out of the usual hash/logging path.
99    #[inline(never)]
100    fn format_padded(&self, f: &mut fmt::Formatter<'_>, upper: bool, width: usize) -> fmt::Result {
101        let prefix = if f.alternate() { "0x" } else { "" };
102        let sign = if f.sign_plus() { "+" } else { "" };
103        // Saturation is sufficient for width comparison: if the complete output
104        // exceeds usize::MAX characters, no representable width adds padding.
105        let len = self
106            .bytes
107            .len()
108            .saturating_mul(2)
109            .saturating_add(prefix.len())
110            .saturating_add(sign.len());
111        let padding = width.saturating_sub(len);
112        let zero_pad = f.sign_aware_zero_pad();
113        let (left, right) = if zero_pad {
114            (0, 0)
115        } else {
116            match f.align().unwrap_or(Alignment::Right) {
117                Alignment::Left => (0, padding),
118                Alignment::Right => (padding, 0),
119                Alignment::Center => (padding / 2, padding - padding / 2),
120            }
121        };
122        let fill = f.fill();
123        write_fill(f, fill, left)?;
124        if !sign.is_empty() {
125            f.write_str(sign)?;
126        }
127        if !prefix.is_empty() {
128            f.write_str(prefix)?;
129        }
130        if zero_pad {
131            write_fill(f, '0', padding)?;
132        }
133
134        self.write_hex(f, upper)?;
135        write_fill(f, fill, right)
136    }
137
138    #[inline]
139    fn write_hex(&self, f: &mut fmt::Formatter<'_>, upper: bool) -> fmt::Result {
140        if self.bytes.is_empty() {
141            return Ok(());
142        }
143        if self.bytes.len() <= 128 {
144            // Hashes and short identifiers need one encode and one writer call.
145            let mut buffer = [MaybeUninit::uninit(); 256];
146            let text = encode(self.bytes, &mut buffer, upper)
147                .expect("short input fits the fixed encoding buffer");
148            return f.write_str(text);
149        }
150        self.write_long(f, upper)
151    }
152
153    // Keep the larger scratch space and streaming loop out of short hash calls.
154    #[inline(never)]
155    fn write_long(&self, f: &mut fmt::Formatter<'_>, upper: bool) -> fmt::Result {
156        let mut buffer = [MaybeUninit::uninit(); 1024];
157        for chunk in self.bytes.chunks(buffer.len() / 2) {
158            // Each chunk is at most half this fixed, nonempty buffer. Its
159            // encoded length cannot overflow or exceed capacity. Keep the
160            // checked encoder's initialization boundary intact.
161            let text = encode(chunk, &mut buffer, upper)
162                .expect("each chunk fits the fixed encoding buffer");
163            f.write_str(text)?;
164        }
165        Ok(())
166    }
167}
168
169fn write_fill(f: &mut fmt::Formatter<'_>, fill: char, mut count: usize) -> fmt::Result {
170    // Common integer padding needs one write per block, not per character.
171    let block = match fill {
172        ' ' => "                                ",
173        '0' => "00000000000000000000000000000000",
174        _ => {
175            for _ in 0..count {
176                f.write_char(fill)?;
177            }
178            return Ok(());
179        }
180    };
181    while count >= block.len() {
182        f.write_str(block)?;
183        count -= block.len();
184    }
185    if count != 0 {
186        f.write_str(&block[..count])?;
187    }
188    Ok(())
189}
190
191impl fmt::Display for Hex<'_> {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        self.format(f, false)
194    }
195}
196
197impl fmt::LowerHex for Hex<'_> {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        self.format(f, false)
200    }
201}
202
203impl fmt::UpperHex for Hex<'_> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        self.format(f, true)
206    }
207}