use std::io::{self, Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressRadix {
Octal,
Decimal,
Hex,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endian {
Little,
Big,
Native,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
NamedChar,
PrintableChar,
SignedDec(usize),
Float(usize),
Octal(usize),
UnsignedDec(usize),
Hex(usize),
}
#[derive(Debug, Clone)]
pub struct OdConfig {
pub address_radix: AddressRadix,
pub formats: Vec<OutputFormat>,
pub z_flags: Vec<bool>,
pub skip_bytes: u64,
pub read_bytes: Option<u64>,
pub width: usize,
pub show_duplicates: bool,
pub endian: Endian,
}
impl Default for OdConfig {
fn default() -> Self {
Self {
address_radix: AddressRadix::Octal,
formats: vec![OutputFormat::Octal(2)],
z_flags: vec![false],
skip_bytes: 0,
read_bytes: None,
width: 16,
show_duplicates: false,
endian: Endian::Native,
}
}
}
const NAMED_CHARS: [&str; 128] = [
"nul", "soh", "stx", "etx", "eot", "enq", "ack", "bel", " bs", " ht", " nl", " vt", " ff",
" cr", " so", " si", "dle", "dc1", "dc2", "dc3", "dc4", "nak", "syn", "etb", "can", " em",
"sub", "esc", " fs", " gs", " rs", " us", " sp", "!", "\"", "#", "$", "%", "&", "'", "(", ")",
"*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<",
"=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
"P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b",
"c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
"v", "w", "x", "y", "z", "{", "|", "}", "~", "del",
];
fn field_width(fmt: OutputFormat) -> usize {
match fmt {
OutputFormat::NamedChar => 4, OutputFormat::PrintableChar => 4, OutputFormat::Octal(1) => 4, OutputFormat::Octal(2) => 7, OutputFormat::Octal(4) => 12, OutputFormat::Octal(8) => 23, OutputFormat::Hex(1) => 3, OutputFormat::Hex(2) => 5, OutputFormat::Hex(4) => 9, OutputFormat::Hex(8) => 17, OutputFormat::UnsignedDec(1) => 4, OutputFormat::UnsignedDec(2) => 6, OutputFormat::UnsignedDec(4) => 11, OutputFormat::UnsignedDec(8) => 21, OutputFormat::SignedDec(1) => 5, OutputFormat::SignedDec(2) => 7, OutputFormat::SignedDec(4) => 12, OutputFormat::SignedDec(8) => 21, OutputFormat::Float(4) => 16, OutputFormat::Float(8) => 25, _ => 4,
}
}
fn element_size(fmt: OutputFormat) -> usize {
match fmt {
OutputFormat::NamedChar | OutputFormat::PrintableChar => 1,
OutputFormat::SignedDec(s)
| OutputFormat::Float(s)
| OutputFormat::Octal(s)
| OutputFormat::UnsignedDec(s)
| OutputFormat::Hex(s) => s,
}
}
fn snprintf_g(v: f64, precision: usize) -> String {
let precision = precision.min(50);
#[cfg(unix)]
{
static FMT_STRINGS: &[&std::ffi::CStr] = &[
c"%.0g", c"%.1g", c"%.2g", c"%.3g", c"%.4g", c"%.5g", c"%.6g", c"%.7g", c"%.8g",
c"%.9g", c"%.10g", c"%.11g", c"%.12g", c"%.13g", c"%.14g", c"%.15g", c"%.16g",
c"%.17g", c"%.18g", c"%.19g", c"%.20g",
];
let mut buf = [0u8; 64];
let fmt_cstr: std::ffi::CString;
let fmt_ptr = if precision < FMT_STRINGS.len() {
FMT_STRINGS[precision].as_ptr()
} else {
fmt_cstr = std::ffi::CString::new(format!("%.{}g", precision)).unwrap();
fmt_cstr.as_ptr()
};
let len =
unsafe { libc::snprintf(buf.as_mut_ptr() as *mut libc::c_char, buf.len(), fmt_ptr, v) };
if len > 0 && (len as usize) < buf.len() {
return String::from_utf8_lossy(&buf[..len as usize]).into_owned();
}
}
let s = format!("{:.prec$e}", v, prec = precision.saturating_sub(1));
if let Some(e_pos) = s.find('e') {
let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0);
if exp >= -(precision as i32) && exp < precision as i32 {
let fixed = format!(
"{:.prec$}",
v,
prec = (precision as i32 - 1 - exp).max(0) as usize
);
if fixed.contains('.') {
let trimmed = fixed.trim_end_matches('0').trim_end_matches('.');
return trimmed.to_string();
}
return fixed;
}
}
format!("{:.*e}", precision.saturating_sub(1), v)
}
fn format_float_f32(v: f32) -> String {
for prec in 6usize..=9 {
let s = snprintf_g(v as f64, prec);
if let Ok(reparsed) = s.trim().parse::<f32>() {
if reparsed == v {
return s;
}
}
}
snprintf_g(v as f64, 9)
}
fn format_float_f64(v: f64) -> String {
for prec in 15usize..=17 {
let s = snprintf_g(v, prec);
if let Ok(reparsed) = s.trim().parse::<f64>() {
if reparsed.to_bits() == v.to_bits() {
return s;
}
}
}
snprintf_g(v, 17)
}
#[inline]
fn read_u16(bytes: &[u8], endian: Endian) -> u16 {
let arr: [u8; 2] = bytes[..2].try_into().unwrap();
match endian {
Endian::Big => u16::from_be_bytes(arr),
Endian::Little | Endian::Native => u16::from_le_bytes(arr),
}
}
#[inline]
fn read_u32(bytes: &[u8], endian: Endian) -> u32 {
let arr: [u8; 4] = bytes[..4].try_into().unwrap();
match endian {
Endian::Big => u32::from_be_bytes(arr),
Endian::Little | Endian::Native => u32::from_le_bytes(arr),
}
}
#[inline]
fn read_u64(bytes: &[u8], endian: Endian) -> u64 {
let arr: [u8; 8] = bytes[..8].try_into().unwrap();
match endian {
Endian::Big => u64::from_be_bytes(arr),
Endian::Little | Endian::Native => u64::from_le_bytes(arr),
}
}
static HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
#[inline]
fn fmt_octal(mut v: u64, buf: &mut [u8], digits: usize) -> usize {
let mut i = digits;
while i > 0 {
i -= 1;
buf[i] = b'0' + (v & 7) as u8;
v >>= 3;
}
digits
}
#[inline]
fn fmt_hex(mut v: u64, buf: &mut [u8], digits: usize) -> usize {
let mut i = digits;
while i > 0 {
i -= 1;
buf[i] = HEX_DIGITS[(v & 0xF) as usize];
v >>= 4;
}
digits
}
#[inline]
fn fmt_unsigned(mut v: u64, buf: &mut [u8]) -> usize {
if v == 0 {
buf[0] = b'0';
return 1;
}
let mut i = 0;
while v > 0 {
buf[i] = b'0' + (v % 10) as u8;
v /= 10;
i += 1;
}
buf[..i].reverse();
i
}
#[inline]
fn fmt_signed(v: i64, buf: &mut [u8]) -> usize {
if v >= 0 {
return fmt_unsigned(v as u64, buf);
}
buf[0] = b'-';
let len = fmt_unsigned((-(v as i128)) as u64, &mut buf[1..]);
1 + len
}
#[inline]
fn write_padded(
out: &mut impl Write,
value_buf: &[u8],
value_len: usize,
width: usize,
) -> io::Result<()> {
const SPACES: [u8; 32] = [b' '; 32];
let pad = width.saturating_sub(value_len);
let mut remaining = pad;
while remaining > 0 {
let chunk = remaining.min(SPACES.len());
out.write_all(&SPACES[..chunk])?;
remaining -= chunk;
}
out.write_all(&value_buf[..value_len])
}
#[inline]
fn write_value(
out: &mut impl Write,
bytes: &[u8],
fmt: OutputFormat,
width: usize,
endian: Endian,
) -> io::Result<()> {
let mut buf = [0u8; 24]; match fmt {
OutputFormat::NamedChar => {
let b = bytes[0];
if b < 128 {
let s = NAMED_CHARS[b as usize].as_bytes();
write_padded(out, s, s.len(), width)
} else {
let len = fmt_octal(b as u64, &mut buf, 3);
write_padded(out, &buf, len, width)
}
}
OutputFormat::PrintableChar => {
let b = bytes[0];
let s: &[u8] = match b {
0x00 => b"\\0",
0x07 => b"\\a",
0x08 => b"\\b",
0x09 => b"\\t",
0x0a => b"\\n",
0x0b => b"\\v",
0x0c => b"\\f",
0x0d => b"\\r",
_ => b"",
};
if !s.is_empty() {
write_padded(out, s, s.len(), width)
} else if (0x20..=0x7e).contains(&b) {
buf[0] = b;
write_padded(out, &buf, 1, width)
} else {
buf[0] = b'0' + (b >> 6);
buf[1] = b'0' + ((b >> 3) & 7);
buf[2] = b'0' + (b & 7);
write_padded(out, &buf, 3, width)
}
}
OutputFormat::Octal(size) => {
let (v, digits) = match size {
1 => (bytes[0] as u64, 3),
2 => (read_u16(bytes, endian) as u64, 6),
4 => (read_u32(bytes, endian) as u64, 11),
8 => (read_u64(bytes, endian), 22),
_ => return Ok(()),
};
let len = fmt_octal(v, &mut buf, digits);
write_padded(out, &buf, len, width)
}
OutputFormat::Hex(size) => {
let (v, digits) = match size {
1 => (bytes[0] as u64, 2),
2 => (read_u16(bytes, endian) as u64, 4),
4 => (read_u32(bytes, endian) as u64, 8),
8 => (read_u64(bytes, endian), 16),
_ => return Ok(()),
};
let len = fmt_hex(v, &mut buf, digits);
write_padded(out, &buf, len, width)
}
OutputFormat::UnsignedDec(size) => {
let v = match size {
1 => bytes[0] as u64,
2 => read_u16(bytes, endian) as u64,
4 => read_u32(bytes, endian) as u64,
8 => read_u64(bytes, endian),
_ => return Ok(()),
};
let len = fmt_unsigned(v, &mut buf);
write_padded(out, &buf, len, width)
}
OutputFormat::SignedDec(size) => {
let v: i64 = match size {
1 => bytes[0] as i8 as i64,
2 => read_u16(bytes, endian) as i16 as i64,
4 => read_u32(bytes, endian) as i32 as i64,
8 => read_u64(bytes, endian) as i64,
_ => return Ok(()),
};
let len = fmt_signed(v, &mut buf);
write_padded(out, &buf, len, width)
}
OutputFormat::Float(size) => match size {
4 => {
let v = f32::from_bits(read_u32(bytes, endian));
write!(out, "{:>w$}", format_float_f32(v), w = width)
}
8 => {
let v = f64::from_bits(read_u64(bytes, endian));
write!(out, "{:>w$}", format_float_f64(v), w = width)
}
_ => Ok(()),
},
}
}
fn compute_effective_widths(formats: &[OutputFormat], line_width: usize) -> Vec<usize> {
if formats.len() <= 1 {
return formats.iter().map(|f| field_width(*f)).collect();
}
let mut max_chars_per_block = 0usize;
for fmt in formats {
let es = element_size(*fmt);
let fw = field_width(*fmt);
let num_elems = line_width / es;
let chars = num_elems * fw;
if chars > max_chars_per_block {
max_chars_per_block = chars;
}
}
formats
.iter()
.map(|fmt| {
let es = element_size(*fmt);
let num_elems = line_width / es;
if num_elems > 0 {
max_chars_per_block / num_elems
} else {
field_width(*fmt)
}
})
.collect()
}
fn write_format_line(
out: &mut impl Write,
chunk: &[u8],
fmt: OutputFormat,
line_width: usize,
is_first_format: bool,
radix: AddressRadix,
offset: u64,
z_annotate: bool,
effective_fw: usize,
endian: Endian,
) -> io::Result<()> {
if is_first_format {
let mut addr_buf = [0u8; 22];
match radix {
AddressRadix::Octal => {
let len = fmt_octal(offset, &mut addr_buf, 7);
out.write_all(&addr_buf[..len])?;
}
AddressRadix::Decimal => {
let mut tmp = [0u8; 20];
let vlen = fmt_unsigned(offset, &mut tmp);
let pad = 7usize.saturating_sub(vlen);
for b in addr_buf.iter_mut().take(pad) {
*b = b'0';
}
addr_buf[pad..pad + vlen].copy_from_slice(&tmp[..vlen]);
out.write_all(&addr_buf[..pad + vlen])?;
}
AddressRadix::Hex => {
let len = fmt_hex(offset, &mut addr_buf, 6);
out.write_all(&addr_buf[..len])?;
}
AddressRadix::None => {}
}
} else if radix != AddressRadix::None {
let addr_width = match radix {
AddressRadix::Octal | AddressRadix::Decimal => 7,
AddressRadix::Hex => 6,
AddressRadix::None => 0,
};
for _ in 0..addr_width {
out.write_all(b" ")?;
}
}
let elem_sz = element_size(fmt);
let fw = effective_fw;
let num_elems = line_width / elem_sz;
let actual_full = chunk.len() / elem_sz;
let remainder = chunk.len() % elem_sz;
for i in 0..num_elems {
if i < actual_full {
let start = i * elem_sz;
let end = start + elem_sz;
write_value(out, &chunk[start..end], fmt, fw, endian)?;
} else if i == actual_full && remainder > 0 {
let start = i * elem_sz;
let mut padded = [0u8; 8]; padded[..remainder].copy_from_slice(&chunk[start..]);
write_value(out, &padded[..elem_sz], fmt, fw, endian)?;
}
}
if z_annotate {
let used_cols = actual_full + if remainder > 0 { 1 } else { 0 };
for _ in used_cols..num_elems {
for _ in 0..fw {
out.write_all(b" ")?;
}
}
out.write_all(b" >")?;
for &b in chunk {
if b.is_ascii_graphic() || b == b' ' {
out.write_all(&[b])?;
} else {
out.write_all(b".")?;
}
}
out.write_all(b"<")?;
}
writeln!(out)?;
Ok(())
}
pub fn parse_format_type(s: &str) -> Result<(OutputFormat, bool), String> {
if s.is_empty() {
return Err("empty format string".to_string());
}
let (s, z_annotate) = if s.len() > 1 && s.ends_with('z') {
(&s[..s.len() - 1], true)
} else {
(s, false)
};
let mut chars = s.chars();
let type_char = chars.next().unwrap();
let size_str: String = chars.collect();
let fmt = match type_char {
'a' => Ok(OutputFormat::NamedChar),
'c' => Ok(OutputFormat::PrintableChar),
'd' => {
let size = if size_str.is_empty() {
4
} else {
parse_size_spec(&size_str, "d")?
};
Ok(OutputFormat::SignedDec(size))
}
'f' => {
let size = if size_str.is_empty() {
4
} else {
parse_float_size(&size_str)?
};
Ok(OutputFormat::Float(size))
}
'o' => {
let size = if size_str.is_empty() {
2
} else {
parse_size_spec(&size_str, "o")?
};
Ok(OutputFormat::Octal(size))
}
'u' => {
let size = if size_str.is_empty() {
4
} else {
parse_size_spec(&size_str, "u")?
};
Ok(OutputFormat::UnsignedDec(size))
}
'x' => {
let size = if size_str.is_empty() {
2
} else {
parse_size_spec(&size_str, "x")?
};
Ok(OutputFormat::Hex(size))
}
_ => Err(format!("invalid type string '{}'", s)),
}?;
Ok((fmt, z_annotate))
}
fn parse_size_spec(s: &str, type_name: &str) -> Result<usize, String> {
match s {
"C" => Ok(1),
"S" => Ok(2),
"I" => Ok(4),
"L" => Ok(8),
_ => {
let n: usize = s
.parse()
.map_err(|_| format!("invalid type string '{}{}': invalid size", type_name, s))?;
match n {
1 | 2 | 4 | 8 => Ok(n),
_ => Err(format!(
"invalid type string '{}{}': invalid size",
type_name, s
)),
}
}
}
}
fn parse_float_size(s: &str) -> Result<usize, String> {
match s {
"F" | "4" => Ok(4),
"D" | "8" => Ok(8),
"L" | "16" => Err("16-byte float not supported".to_string()),
_ => {
let n: usize = s
.parse()
.map_err(|_| format!("invalid float size '{}'", s))?;
match n {
4 | 8 => Ok(n),
_ => Err(format!("invalid float size '{}'", s)),
}
}
}
}
pub fn od_process<R: Read, W: Write>(
mut input: R,
output: &mut W,
config: &OdConfig,
) -> io::Result<()> {
if config.skip_bytes > 0 {
let mut to_skip = config.skip_bytes;
let mut skip_buf = [0u8; 8192];
while to_skip > 0 {
let chunk_size = std::cmp::min(to_skip, skip_buf.len() as u64) as usize;
let n = input.read(&mut skip_buf[..chunk_size])?;
if n == 0 {
break;
}
to_skip -= n as u64;
}
}
let data = match config.read_bytes {
Some(limit) => {
let mut buf = Vec::new();
let mut limited = input.take(limit);
limited.read_to_end(&mut buf)?;
buf
}
None => {
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
buf
}
};
let width = config.width;
let mut offset = config.skip_bytes;
let mut prev_chunk: Option<Vec<u8>> = None;
let mut star_printed = false;
let effective_widths = compute_effective_widths(&config.formats, width);
let mut pos = 0;
while pos < data.len() {
let end = std::cmp::min(pos + width, data.len());
let chunk = &data[pos..end];
if !config.show_duplicates && chunk.len() == width {
if let Some(ref prev) = prev_chunk {
if prev.as_slice() == chunk {
if !star_printed {
writeln!(output, "*")?;
star_printed = true;
}
pos += width;
offset += width as u64;
continue;
}
}
}
star_printed = false;
for (i, fmt) in config.formats.iter().enumerate() {
let z = config.z_flags.get(i).copied().unwrap_or(false);
let ew = effective_widths[i];
write_format_line(
output,
chunk,
*fmt,
width,
i == 0,
config.address_radix,
offset,
z,
ew,
config.endian,
)?;
}
prev_chunk = Some(chunk.to_vec());
pos += width;
offset += width as u64;
}
if config.address_radix != AddressRadix::None {
let final_offset = config.skip_bytes + data.len() as u64;
match config.address_radix {
AddressRadix::Octal => writeln!(output, "{:07o}", final_offset)?,
AddressRadix::Decimal => writeln!(output, "{:07}", final_offset)?,
AddressRadix::Hex => writeln!(output, "{:06x}", final_offset)?,
AddressRadix::None => {}
}
}
Ok(())
}