pub fn format_hex(buf: &[u8]) -> String {
format_hex_with_prefix_and_separator(buf, "", " ", 16, 8)
}
pub fn format_hex_with_prefix_and_separator(
buf: &[u8],
prefix: &str,
separator: &str,
octets_per_line: usize,
group_size: usize,
) -> String {
assert!(
group_size > 0 && group_size <= octets_per_line,
"group_size must be greater than 0 and less than or equal to octets_per_line"
);
let mut result = String::new();
for (i, chunk) in buf.chunks(octets_per_line).enumerate() {
let offset = i * octets_per_line;
result.push_str(&format!("{:08X} ", offset));
let mut ascii_repr = String::new();
for (j, byte) in chunk.iter().enumerate() {
if j > 0 && j % group_size == 0 {
result.push(' '); result.push(' '); } else if j > 0 {
result.push_str(separator); }
result.push_str(&format!("{prefix}{:02X}", byte));
if byte.is_ascii_graphic() || byte == &b' ' {
ascii_repr.push(*byte as char);
} else {
ascii_repr.push('.'); }
}
if chunk.len() < octets_per_line {
let missing_bytes = octets_per_line - chunk.len();
let extra_spaces = if group_size > 0 {
let full_groups_missing = missing_bytes / group_size;
let remaining_bytes = missing_bytes % group_size;
let per_byte_space = prefix.len() + 2 + separator.len();
let per_group_space = 1;
full_groups_missing * (group_size * per_byte_space + per_group_space)
+ remaining_bytes * per_byte_space
} else {
missing_bytes * (prefix.len() + 2 + separator.len())
};
result.push_str(&" ".repeat(extra_spaces));
}
result.push_str(" ");
result.push_str(&ascii_repr);
if i < (buf.len() + octets_per_line - 1) / octets_per_line - 1 {
result.push('\n');
}
}
result
}
pub fn format_hex_u32_be(num: u32) -> String {
format_hex_with_prefix_and_separator(&num.to_be_bytes(), "0x", ",", 16, 8)
}
pub fn format_hex_dump_comparison(buf1: &[u8], buf2: &[u8]) -> String {
format_hex_dump_comparison_interleaved(buf1, buf2, "", " ", 16, 8)
}
fn compute_group_size(octets_per_line: usize) -> usize {
1 << ((octets_per_line / 2).clamp(4, 16).ilog2())
}
pub fn format_hex_dump_comparison_width(
encountered_buf: &[u8],
expected_buf: &[u8],
octets_per_line: usize,
) -> String {
let group_size = compute_group_size(octets_per_line);
format_hex_dump_comparison_interleaved(
encountered_buf,
expected_buf,
"",
" ",
octets_per_line,
group_size,
)
}
fn format_hex_dump_comparison_interleaved(
encountered_buf: &[u8],
expected_buf: &[u8],
prefix: &str,
separator: &str,
bytes_per_line: usize,
group_size: usize,
) -> String {
assert!(
group_size > 0 && group_size <= bytes_per_line,
"group_size must be greater than 0 and less than or equal to bytes_per_line"
);
let mut result = String::new();
let total_lines =
(expected_buf.len().max(encountered_buf.len()) + bytes_per_line - 1) / bytes_per_line;
for line in 0..total_lines {
let offset = line * bytes_per_line;
let offset_str = format!("{:08X}", offset);
let expected_chunk = if line * bytes_per_line >= expected_buf.len() {
&[]
} else {
&expected_buf
[line * bytes_per_line..expected_buf.len().min((line + 1) * bytes_per_line)]
};
let encountered_chunk = if line * bytes_per_line >= encountered_buf.len() {
&[]
} else {
&encountered_buf
[line * bytes_per_line..encountered_buf.len().min((line + 1) * bytes_per_line)]
};
let formatted_hex1 = format_hex_octets(expected_chunk, prefix, separator, group_size);
let ascii1 = format_ascii(expected_chunk);
let formatted_hex2 = format_hex_bytes_with_diff(
encountered_chunk,
expected_chunk,
prefix,
separator,
group_size,
);
let ascii2 = format_ascii(encountered_chunk);
if !expected_chunk.is_empty() {
result.push_str(&format!(
"{:<10} {:<} {}!\n",
offset_str, formatted_hex1, ascii1
));
}
if !encountered_chunk.is_empty() {
result.push_str(&format!(
"{:<10} {:<} {}!\n",
offset_str, formatted_hex2, ascii2
));
}
}
if result.ends_with('\n') {
result.pop();
}
result
}
fn format_hex_octets(buf: &[u8], prefix: &str, separator: &str, group_size: usize) -> String {
let mut hex_str = String::new();
for (j, octet) in buf.iter().enumerate() {
if j > 0 && j % group_size == 0 {
hex_str.push(' '); hex_str.push(' '); } else if j > 0 {
hex_str.push_str(separator); }
hex_str.push_str(&format!("{prefix}{:02X}", octet));
}
hex_str
}
fn format_hex_bytes_with_diff(
encountered_buf: &[u8],
expected_buf: &[u8],
prefix: &str,
separator: &str,
group_size: usize,
) -> String {
let mut hex_str = String::new();
for (j, octet) in encountered_buf.iter().enumerate() {
if j > 0 && j % group_size == 0 {
hex_str.push(' '); hex_str.push(' '); } else if j > 0 {
hex_str.push_str(separator); }
let differing = j < expected_buf.len() && octet != &expected_buf[j];
if differing {
hex_str.push_str("\x1b[31m"); }
hex_str.push_str(&format!("{prefix}{:02X}", octet));
if differing {
hex_str.push_str("\x1b[0m"); }
}
hex_str
}
fn format_ascii(buf: &[u8]) -> String {
buf.iter()
.map(|&b| {
if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
}
})
.collect()
}
pub fn assert_eq_slices(buf_to_test: &[u8], expected_buf: &[u8]) {
compare_eq_slices(buf_to_test, expected_buf)
.unwrap_or_else(|err| panic!("octet slices are not equal!\n\nComparison:\n{}", err));
}
pub fn compare_eq_slices(buf_to_test: &[u8], expected_buf: &[u8]) -> Result<(), String> {
if buf_to_test == expected_buf {
#[cfg(feature = "log_equal")]
{
use log::trace;
trace!("assert: slices are equal: {}", format_hex(buf_to_test));
}
Ok(())
} else {
let formatted_dump = format_hex_dump_comparison_width(buf_to_test, expected_buf, 16);
Err(format!(
"octet slices are not equal!\n\nComparison:\n{}",
formatted_dump
))
}
}