Skip to content

Commit b4b410d

Browse files
committed
fix: preserve case and padding on truncated hex formatting
Fixes a bug where hex slices formatted with precision and uppercase specifiers (e.g., `{:.4X}`) incorrectly output lowercase prefix bytes. Creates a buffer on the stack and delegates formatting to `f.pad()` to preserve system alignment/padding flags. Closes #249
1 parent 0799a10 commit b4b410d

1 file changed

Lines changed: 86 additions & 4 deletions

File tree

src/display.rs

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use alloc::string::String;
3030
use core::borrow::Borrow;
3131
use core::fmt;
3232

33+
use fmt::Write;
34+
3335
use super::Case;
3436
#[cfg(feature = "std")]
3537
use super::Table;
@@ -106,6 +108,48 @@ pub trait DisplayHex {
106108
fn hex_reserve_suggestion(&self) -> usize;
107109
}
108110

111+
/// Formats and writes a truncated hex slice directly into the stack buffer
112+
/// based on the requested precision limit and character case.
113+
struct TruncatedHexBuf {
114+
buf: [u8; 1024],
115+
len: usize,
116+
}
117+
118+
impl TruncatedHexBuf {
119+
fn new() -> Self { Self { buf: [0; 1024], len: 0 } }
120+
121+
fn as_str(&self) -> &str { core::str::from_utf8(&self.buf[..self.len]).unwrap() }
122+
123+
fn write_truncated_hex(&mut self, bytes: &[u8], case: Case, max: usize) -> fmt::Result {
124+
let prefix_len = max / 2;
125+
126+
for &b in &bytes[..prefix_len] {
127+
let chars = case.table().byte_to_chars(b);
128+
self.write_char(chars[0])?;
129+
self.write_char(chars[1])?;
130+
}
131+
132+
if max % 2 == 1 {
133+
let chars = case.table().byte_to_chars(bytes[prefix_len]);
134+
self.write_char(chars[0])?;
135+
}
136+
137+
Ok(())
138+
}
139+
}
140+
141+
impl fmt::Write for TruncatedHexBuf {
142+
fn write_str(&mut self, s: &str) -> fmt::Result {
143+
let bytes = s.as_bytes();
144+
if self.len + bytes.len() > self.buf.len() {
145+
return Err(fmt::Error);
146+
}
147+
self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes);
148+
self.len += bytes.len();
149+
Ok(())
150+
}
151+
}
152+
109153
fn internal_display(bytes: &[u8], f: &mut fmt::Formatter, case: Case) -> fmt::Result {
110154
use fmt::Write;
111155
// There are at least two optimizations left:
@@ -123,10 +167,9 @@ fn internal_display(bytes: &[u8], f: &mut fmt::Formatter, case: Case) -> fmt::Re
123167
}
124168
match f.precision() {
125169
Some(max) if bytes.len() > max / 2 => {
126-
write!(f, "{}", bytes[..(max / 2)].as_hex())?;
127-
if max % 2 == 1 {
128-
f.write_char(case.table().byte_to_chars(bytes[max / 2])[0])?;
129-
}
170+
let mut stack_str = TruncatedHexBuf::new();
171+
stack_str.write_truncated_hex(bytes, case, max)?;
172+
return f.pad(stack_str.as_str());
130173
}
131174
Some(_) | None => {
132175
let mut chunks = bytes.chunks_exact(512);
@@ -918,6 +961,45 @@ mod tests {
918961
assert_eq!(bytes.to_upper_hex_string(), upper);
919962
assert_eq!(bytes.to_lower_hex_string(), lower);
920963
}
964+
965+
#[test]
966+
fn upper_hex_precision_preserves_case() {
967+
let bytes: [u8; 4] = [0xab, 0xcd, 0xef, 0x12];
968+
let slice: &[u8] = &bytes;
969+
assert_eq!(format!("{:.4X}", slice.as_hex()), "ABCD");
970+
assert_eq!(format!("{:.5X}", slice.as_hex()), "ABCDE");
971+
}
972+
973+
#[test]
974+
fn lower_hex_precision_works_correctly_with_lowecase() {
975+
let bytes: [u8; 4] = [0xab, 0xcd, 0xef, 0x12];
976+
let slice: &[u8] = &bytes;
977+
assert_eq!(format!("{:.4x}", slice.as_hex()), "abcd");
978+
assert_eq!(format!("{:.5x}", slice.as_hex()), "abcde");
979+
}
980+
981+
#[test]
982+
fn hex_precision_extreme_boundaries() {
983+
let bytes: [u8; 2] = [0xaa, 0xbb];
984+
let slice: &[u8] = &bytes;
985+
986+
// zero precision
987+
assert_eq!(format!("{:.0X}", slice.as_hex()), "");
988+
assert_eq!(format!("{:.0x}", slice.as_hex()), "");
989+
990+
// 1-nibble precision (odd, edge case)
991+
assert_eq!(format!("{:.1X}", slice.as_hex()), "A");
992+
assert_eq!(format!("{:.1x}", slice.as_hex()), "a");
993+
}
994+
995+
#[test]
996+
fn hex_precision_greater_than_length() {
997+
let bytes: [u8; 2] = [0xab, 0xcd];
998+
let slice: &[u8] = &bytes;
999+
1000+
assert_eq!(format!("{:.10X}", slice.as_hex()), "ABCD");
1001+
assert_eq!(format!("{:.10x}", slice.as_hex()), "abcd");
1002+
}
9211003
}
9221004

9231005
#[cfg(feature = "std")]

0 commit comments

Comments
 (0)