#![no_std]
#![doc = include_str!("../README.md")]
use core::fmt::{Debug, Display, LowerHex, UpperHex};
#[cfg(any(feature = "alloc", test))]
extern crate alloc;
pub trait HexDisplayExt: sealed::HexSealed {
#[must_use]
fn hex(&self) -> Hex<'_>;
#[cfg(feature = "alloc")]
#[must_use]
fn to_upper_hex_string(&self) -> alloc::string::String {
alloc::format!("{:X}", self.hex())
}
#[cfg(feature = "alloc")]
#[must_use]
fn to_hex_string(&self) -> alloc::string::String {
use alloc::string::ToString;
self.hex().to_string()
}
#[cfg(feature = "alloc")]
#[must_use]
fn to_upper_hex_dump(&self) -> alloc::string::String {
alloc::format!("{:#X}", self.hex())
}
#[cfg(feature = "alloc")]
#[must_use]
fn to_hex_dump(&self) -> alloc::string::String {
alloc::format!("{:#x}", self.hex())
}
}
impl<T: AsRef<[u8]> + ?Sized> HexDisplayExt for T {
fn hex(&self) -> Hex<'_> {
Hex(self.as_ref())
}
}
mod sealed {
#[allow(clippy::module_name_repetitions)]
pub trait HexSealed {}
impl<T: AsRef<[u8]> + ?Sized> HexSealed for T {}
}
#[derive(Clone, Copy)]
pub struct Hex<'a>(
&'a [u8],
);
impl<'a> Hex<'a> {
#[must_use]
pub fn new(s: &'a (impl AsRef<[u8]> + ?Sized)) -> Self {
Self(s.as_ref())
}
}
const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
fn write_hex(
f: &mut core::fmt::Formatter<'_>,
bytes: &[u8],
table: &[u8; 16],
) -> core::fmt::Result {
const CHUNK: usize = 32;
let mut buf = [0u8; CHUNK * 2];
for chunk in bytes.chunks(CHUNK) {
for (i, &byte) in chunk.iter().enumerate() {
buf[i * 2] = table[(byte >> 4) as usize];
buf[i * 2 + 1] = table[(byte & 0x0f) as usize];
}
let s = core::str::from_utf8(&buf[..chunk.len() * 2])
.expect("hex lookup table only emits ASCII");
f.write_str(s)?;
}
Ok(())
}
const HEXDUMP_LINE_BYTES: usize = 16;
const HEXDUMP_ADDRESS_WIDTH: usize = 8;
const HEXDUMP_ADDRESS_OVERFLOW_LINE_NUM: usize =
1 << (HEXDUMP_ADDRESS_WIDTH * 4 - HEXDUMP_LINE_BYTES.ilog2() as usize);
const HEXDUMP_HEX_REGION: usize = 8 * 3 * 2;
const HEXDUMP_FULL_LINE: usize =
HEXDUMP_ADDRESS_WIDTH + 2 + HEXDUMP_HEX_REGION + 2 + 1 + HEXDUMP_LINE_BYTES + 1;
fn hexdump_len(byte_count: usize) -> usize {
if byte_count == 0 {
return 0;
}
let n_full = byte_count / HEXDUMP_LINE_BYTES;
let rem = byte_count % HEXDUMP_LINE_BYTES;
let n_lines = n_full + usize::from(rem > 0);
let partial = if rem > 0 {
8 + 2 + HEXDUMP_HEX_REGION + 2 + 1 + rem + 1
} else {
0
};
n_full * HEXDUMP_FULL_LINE + partial + n_lines - 1
}
fn write_hexdump(
f: &mut core::fmt::Formatter<'_>,
bytes: &[u8],
table: &[u8; 16],
) -> core::fmt::Result {
for (line_idx, chunk) in bytes.chunks(HEXDUMP_LINE_BYTES).enumerate() {
let mut buf = [b' '; HEXDUMP_FULL_LINE + 1];
let mut buf_idx = 0;
if line_idx > 0 {
buf[buf_idx] = b'\n';
buf_idx += 1;
}
let offset = line_idx * HEXDUMP_LINE_BYTES;
let offset_buf = &mut buf[buf_idx..][..8];
for i in 0..HEXDUMP_ADDRESS_WIDTH {
offset_buf[HEXDUMP_ADDRESS_WIDTH - i - 1] = table[(offset >> (i * 4)) & 0xf];
}
if line_idx >= HEXDUMP_ADDRESS_OVERFLOW_LINE_NUM {
offset_buf[0] = b'*';
}
buf_idx += HEXDUMP_ADDRESS_WIDTH + 2;
let hex_buf = &mut buf[buf_idx..][..HEXDUMP_HEX_REGION];
for (i, &byte) in chunk.iter().enumerate() {
let pos = i * 3 + usize::from(i >= 8);
hex_buf[pos] = table[(byte >> 4) as usize];
hex_buf[pos + 1] = table[(byte & 0x0f) as usize];
}
buf_idx += HEXDUMP_HEX_REGION + 2;
buf[buf_idx] = b'|';
buf_idx += 1;
let ascii_buf = &mut buf[buf_idx..][..chunk.len()];
for (i, &byte) in chunk.iter().enumerate() {
ascii_buf[i] = if (0x20..=0x7e).contains(&byte) {
byte
} else {
b'.'
};
}
buf_idx += chunk.len();
buf[buf_idx] = b'|';
buf_idx += 1;
f.write_str(
core::str::from_utf8(&buf[..buf_idx]).expect("we should not write invalid ascii"),
)?;
}
Ok(())
}
fn write_padded(
f: &mut core::fmt::Formatter<'_>,
content_len: usize,
write_content: impl FnOnce(&mut core::fmt::Formatter<'_>) -> core::fmt::Result,
) -> core::fmt::Result {
use core::fmt::{Alignment, Write};
let Some(width) = f.width() else {
return write_content(f);
};
if content_len >= width {
return write_content(f);
}
let pad_total = width - content_len;
let fill = f.fill();
let align = f.align().unwrap_or(Alignment::Left);
let (pre, post) = match align {
Alignment::Left => (0, pad_total),
Alignment::Right => (pad_total, 0),
Alignment::Center => (pad_total / 2, pad_total - pad_total / 2),
};
for _ in 0..pre {
f.write_char(fill)?;
}
write_content(f)?;
for _ in 0..post {
f.write_char(fill)?;
}
Ok(())
}
fn fmt_hex_with_options(
f: &mut core::fmt::Formatter<'_>,
bytes: &[u8],
table: &[u8; 16],
) -> core::fmt::Result {
let alternate = f.alternate();
let content_len = if alternate {
hexdump_len(bytes.len())
} else {
bytes.len() * 2
};
write_padded(f, content_len, |f| {
if alternate {
write_hexdump(f, bytes, table)
} else {
write_hex(f, bytes, table)
}
})
}
impl UpperHex for Hex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt_hex_with_options(f, self.0, UPPER_HEX)
}
}
impl LowerHex for Hex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt_hex_with_options(f, self.0, LOWER_HEX)
}
}
impl Debug for Hex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("Hex")
.field(&format_args!("{self:x}"))
.finish()
}
}
impl Display for Hex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt_hex_with_options(f, self.0, LOWER_HEX)
}
}
#[cfg(test)]
mod tests {
use alloc::format;
use super::*;
#[test]
fn test_all_bytes() {
for byte in 0..=0xff {
assert_eq!(format!("{byte:02x}"), format!("{}", Hex(&[byte])));
#[cfg(feature = "alloc")]
assert_eq!(format!("{byte:02x}"), [byte].to_hex_string());
assert_eq!(format!("{byte:02x}"), format!("{:x}", Hex(&[byte])));
assert_eq!(format!("{byte:02X}"), format!("{:X}", Hex(&[byte])));
#[cfg(feature = "alloc")]
assert_eq!(format!("{byte:02X}"), [byte].to_upper_hex_string());
}
}
#[test]
fn test_all_byte_pairs() {
for (a, b) in (0..=0xff).zip(0..=0xff) {
assert_eq!(format!("{a:02x}{b:02x}"), format!("{}", Hex(&[a, b])));
assert_eq!(format!("{a:02X}{b:02X}"), format!("{:X}", Hex(&[a, b])));
}
}
#[test]
fn test_width_padding() {
let h = Hex(&[0x01, 0x23]);
assert_eq!(format!("{h:>8}"), " 0123");
assert_eq!(format!("{h:<8}"), "0123 ");
assert_eq!(format!("{h:^8}"), " 0123 ");
assert_eq!(format!("{h:*>8}"), "****0123");
assert_eq!(format!("{h:2}"), "0123");
assert_eq!(format!("{h:8}"), "0123 ");
assert_eq!(format!("{h:>6X}"), " 0123");
}
#[test]
fn test_alternate_single_line() {
let bytes: [u8; 4] = [b'a', b'b', 0x00, 0xff];
let expected = "00000000 61 62 00 ff |ab..|";
assert_eq!(format!("{:#}", Hex(&bytes)), expected);
}
#[test]
fn test_alternate_multiline() {
let mut bytes = [0u8; 18];
#[allow(clippy::cast_possible_truncation)] for (i, b) in bytes.iter_mut().enumerate() {
*b = i as u8;
}
let expected = "\
00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
00000010 10 11 |..|";
assert_eq!(format!("{:#}", Hex(&bytes)), expected);
#[cfg(feature = "alloc")]
assert_eq!(bytes.to_hex_dump(), expected);
}
#[test]
fn test_alternate_upper_and_ascii_gutter() {
let bytes = b"Hello!\x00\x7f";
let expected = "00000000 48 65 6C 6C 6F 21 00 7F |Hello!..|";
assert_eq!(format!("{:#X}", bytes.hex()), expected);
#[cfg(feature = "alloc")]
assert_eq!(bytes.to_upper_hex_dump(), expected);
}
#[test]
fn test_alternate_empty() {
assert_eq!(format!("{:#}", Hex(&[])), "");
}
#[test]
fn test_alternate_with_padding() {
let bytes = [0xabu8, 0xcd];
let dump = format!("{:#}", Hex(&bytes));
assert_eq!(dump.len(), hexdump_len(bytes.len()));
let target_width = dump.len() + 4;
let padded = format!("{:>#1$}", Hex(&bytes), target_width);
assert_eq!(padded.len(), target_width);
assert!(padded.starts_with(" "));
assert_eq!(&padded[4..], dump);
}
}