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}