use std::{fmt::{Display, Debug}};
#[derive(Debug, Clone)]
pub struct HexOutSettings {
pub address_origin: usize,
pub address_width: usize,
pub align_address: bool,
pub big_endian: bool,
pub group_size: usize,
pub groups_per_line: usize,
pub invalid_data_placeholder: char,
pub show_ascii: bool,
pub show_centerline: bool,
pub show_header: bool,
pub show_offset: bool,
pub strict: bool,
pub uppercase: bool,
pub hex_out_error_prefix: Option<String>,
pub hex_out_error_postfix: Option<String>,
}
impl Default for HexOutSettings {
fn default() -> Self {
Self {
address_origin: 0,
address_width: 8,
align_address: true,
big_endian: false,
group_size: 1,
groups_per_line: 16,
invalid_data_placeholder: '?',
show_ascii: true,
show_centerline: true,
show_header: false,
show_offset: true,
strict: false,
uppercase: false,
hex_out_error_prefix: None,
hex_out_error_postfix: None,
}
}
}
#[derive(Clone)]
pub enum HexOutError {
InvalidGroupSize,
UnalignedOffset { offset: usize, group_size: usize },
InvalidAddressWidth,
}
impl Display for HexOutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HexOutError::InvalidGroupSize => write!(f, "Invalid group size (must be 1-16)"),
HexOutError::UnalignedOffset { offset, group_size } => {
write!(
f,
"Offset {} does not align with group size {} in strict mode (offset % group_size = {})",
offset, group_size, offset % group_size
)
},
HexOutError::InvalidAddressWidth => write!(f, "Invalid address width (must be 2-16)"),
}
}
}
impl Debug for HexOutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HexOutError::InvalidGroupSize => write!(f, "HexOutError::InvalidGroupSize"),
HexOutError::UnalignedOffset { offset, group_size } => {
write!(f, "HexOutError::UnalignedOffset {{ offset: {offset}, group_size: {group_size} }}")
}
HexOutError::InvalidAddressWidth => write!(f, "HexOutError::InvalidAddressWidth"),
}
}
}
impl std::error::Error for HexOutError {}
pub trait HexOut {
fn hex_out(&self) -> Result<String, HexOutError>;
fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError>;
fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError>;
fn hex_out_lines_with_settings(
&self,
settings: HexOutSettings,
start_line: usize,
line_count: usize,
) -> Result<String, HexOutError>;
}
impl HexOut for &[u8] {
fn hex_out(&self) -> Result<String, HexOutError> {
hex_out(self, &HexOutSettings::default(), 0, 0, 0)
}
fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError> {
hex_out(self, &HexOutSettings::default(), 0, start_line, line_count)
}
fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError> {
hex_out(self, &settings, 0, 0, 0)
}
fn hex_out_lines_with_settings(
&self,
settings: HexOutSettings,
start_line: usize,
line_count: usize,
) -> Result<String, HexOutError> {
hex_out(self, &settings, 0, start_line, line_count)
}
}
impl HexOut for Vec<u8> {
fn hex_out(&self) -> Result<String, HexOutError> {
self.as_slice().hex_out()
}
fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError> {
self.as_slice().hex_out_lines(start_line, line_count)
}
fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError> {
self.as_slice().hex_out_with_settings(settings)
}
fn hex_out_lines_with_settings(
&self,
settings: HexOutSettings,
start_line: usize,
line_count: usize,
) -> Result<String, HexOutError> {
self.as_slice().hex_out_lines_with_settings(settings, start_line, line_count)
}
}
pub fn hex_out(
data: &[u8],
settings: &HexOutSettings,
offset: usize,
start_line: usize,
line_count: usize,
) -> Result<String, HexOutError> {
if settings.group_size == 0 || settings.group_size > 16 {
return Err(HexOutError::InvalidGroupSize);
}
if settings.address_width < 2 || settings.address_width > 16 {
return Err(HexOutError::InvalidAddressWidth);
}
if settings.strict && (offset % settings.group_size != 0) {
return Err(HexOutError::UnalignedOffset {
offset,
group_size: settings.group_size
});
}
let total_bytes_per_line = settings.group_size * settings.groups_per_line;
let mut line = String::with_capacity(total_bytes_per_line * 3);
let mut ascii = String::with_capacity(total_bytes_per_line);
let last_line_offset = if line_count == 0 {
data.len()
} else {
((start_line + line_count) * total_bytes_per_line + offset).min(data.len())
};
let last_line_offset = last_line_offset
+ (settings.group_size - (last_line_offset % settings.group_size)) % settings.group_size;
let mut result = String::with_capacity(total_bytes_per_line * line_count * 5);
if settings.show_header {
let mut header = String::with_capacity(total_bytes_per_line * 3);
if settings.show_offset {
if settings.address_width >= 8 {
header.push_str("Address: ");
header.push_str(&" ".repeat(settings.address_width - 8));
} else if settings.address_width >= 4 {
header.push_str("Addr: ");
header.push_str(&" ".repeat(settings.address_width - 4));
} else {
header.push_str(&" ".repeat(settings.address_width));
}
}
for group in 0..settings.groups_per_line {
if group > 0 {
header.push(' ');
}
if group == settings.groups_per_line / 2 && settings.show_centerline {
header.push(' ');
}
let col_index = group * settings.group_size;
header.push_str(&" ".repeat(settings.group_size * 2 - 2));
if settings.uppercase {
header.push_str(&format!("{col_index:02X}"));
} else {
header.push_str(&format!("{col_index:02x}"));
}
}
result.push_str(&header);
result.push('\n');
}
let mut cursor = if settings.align_address { 0 } else { offset };
let mut group_index = 0;
let mut group_byte_index = 0;
let mut group_value: u128 = 0;
let mut out_of_bounds_count = 0;
cursor += start_line * total_bytes_per_line;
let mut addr = if settings.align_address {
cursor - (cursor % (settings.group_size * settings.groups_per_line))
} else {
cursor
} + settings.address_origin;
while cursor < last_line_offset {
let byte = if let Some(b) = data.get(cursor) {
*b
} else {
out_of_bounds_count += 1;
0
};
if settings.show_ascii {
if out_of_bounds_count > 0 || cursor < offset {
ascii.push(' ');
} else if (0x20..0x7F).contains(&byte) {
ascii.push(byte as char);
} else {
ascii.push('.');
}
}
if settings.big_endian {
group_value = (group_value << 8) | byte as u128;
} else {
group_value |= (byte as u128) << (8 * group_byte_index);
}
group_byte_index += 1;
if group_byte_index >= settings.group_size {
if !line.is_empty() {
line.push(' ');
}
if cursor < offset {
line.push_str(" ".repeat(settings.group_size).as_str());
} else {
let mut value = if settings.uppercase {
format!("{group_value:0width$X}", width = settings.group_size * 2)
} else {
format!("{group_value:0width$x}", width = settings.group_size * 2)
};
if cursor.saturating_sub(group_byte_index) < offset || out_of_bounds_count > 0 {
let missing_bytes = if out_of_bounds_count > 0 {
out_of_bounds_count
} else {
settings.group_size - (group_byte_index - cursor % settings.group_size)
};
let replace_chars = (missing_bytes * 2).min(value.len());
let mut replacement = settings.invalid_data_placeholder.to_string().repeat(replace_chars);
if let Some(prefix) = &settings.hex_out_error_prefix
{
replacement = format!("{prefix}{replacement}");
}
if let Some(postfix) = &settings.hex_out_error_postfix
{
replacement = format!("{replacement}{postfix}");
}
if settings.big_endian != (out_of_bounds_count > 0) {
value.replace_range(0..replace_chars, &replacement);
} else {
let start = value.len() - replace_chars;
value.replace_range(start..value.len(), &replacement);
}
}
line.push_str(&value);
}
group_index += 1;
let is_last_line = cursor + 1 == last_line_offset;
if group_index == settings.groups_per_line / 2 && settings.show_centerline {
if settings.show_ascii {
ascii.push(' ');
}
line.push(' ');
}
if group_index == settings.groups_per_line || is_last_line || out_of_bounds_count > 0 {
if settings.show_offset {
result.push_str(&format!(
"{:0width$x}: ",
addr,
width = settings.address_width
));
}
if (is_last_line || out_of_bounds_count > 0) && settings.show_ascii {
let centerline_size = if settings.show_centerline { 1 } else { 0 };
let pad_length = ((settings.group_size * 2 + 1) * settings.groups_per_line + centerline_size - 1).saturating_sub(line.len());
let ascii_pad_length = (total_bytes_per_line + centerline_size).saturating_sub(ascii.len());
line.push_str(
&" ".repeat(pad_length),
);
if settings.show_ascii {
ascii.push_str(
&" ".repeat(ascii_pad_length),
);
}
}
result.push_str(&line);
if settings.show_ascii {
result.push(' ');
result.push('|');
result.push_str(&ascii);
result.push('|');
}
if !is_last_line {
result.push('\n');
}
if out_of_bounds_count > 0 {
break;
}
line.clear();
ascii.clear();
addr += total_bytes_per_line;
group_index = 0;
}
group_value = 0;
group_byte_index = 0;
}
cursor += 1;
}
Ok(result)
}
#[cfg(test)]
mod tests;