Skip to main content

coreutils_rs/od/
core.rs

1use std::io::{self, Read, Write};
2
3/// Address radix for the offset column.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum AddressRadix {
6    Octal,
7    Decimal,
8    Hex,
9    None,
10}
11
12/// Byte order for multi-byte values.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Endian {
15    Little,
16    Big,
17    Native,
18}
19
20/// Output format specifier.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum OutputFormat {
23    /// Named character (a): nul, soh, stx, ...
24    NamedChar,
25    /// Printable character or backslash escape (c): \0, \a, \b, \t, \n, ...
26    PrintableChar,
27    /// Signed decimal integer of given byte size (d1, d2, d4, d8)
28    SignedDec(usize),
29    /// Floating point of given byte size (f4, f8)
30    Float(usize),
31    /// Octal integer of given byte size (o1, o2, o4)
32    Octal(usize),
33    /// Unsigned decimal integer of given byte size (u1, u2, u4, u8)
34    UnsignedDec(usize),
35    /// Hexadecimal integer of given byte size (x1, x2, x4, x8)
36    Hex(usize),
37}
38
39/// Configuration for the od command.
40#[derive(Debug, Clone)]
41pub struct OdConfig {
42    pub address_radix: AddressRadix,
43    pub formats: Vec<OutputFormat>,
44    /// Per-format flag: if true, append printable ASCII annotation (the 'z' suffix).
45    pub z_flags: Vec<bool>,
46    pub skip_bytes: u64,
47    pub read_bytes: Option<u64>,
48    pub width: usize,
49    pub show_duplicates: bool,
50    pub endian: Endian,
51}
52
53impl Default for OdConfig {
54    fn default() -> Self {
55        Self {
56            address_radix: AddressRadix::Octal,
57            formats: vec![OutputFormat::Octal(2)],
58            z_flags: vec![false],
59            skip_bytes: 0,
60            read_bytes: None,
61            width: 16,
62            show_duplicates: false,
63            endian: Endian::Native,
64        }
65    }
66}
67
68/// Named characters for -t a format (ASCII named characters).
69/// Index 0..=127 maps to the name for that byte value.
70const NAMED_CHARS: [&str; 128] = [
71    "nul", "soh", "stx", "etx", "eot", "enq", "ack", "bel", " bs", " ht", " nl", " vt", " ff",
72    " cr", " so", " si", "dle", "dc1", "dc2", "dc3", "dc4", "nak", "syn", "etb", "can", " em",
73    "sub", "esc", " fs", " gs", " rs", " us", " sp", "!", "\"", "#", "$", "%", "&", "'", "(", ")",
74    "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<",
75    "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
76    "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b",
77    "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
78    "v", "w", "x", "y", "z", "{", "|", "}", "~", "del",
79];
80
81/// Return the field width for a single value of the given format.
82/// This matches GNU od's column widths.
83fn field_width(fmt: OutputFormat) -> usize {
84    match fmt {
85        OutputFormat::NamedChar => 4, // 3 chars + leading space => " nul" = 4 wide
86        OutputFormat::PrintableChar => 4, // 3 chars + leading space => "  \\n" = 4 wide
87        OutputFormat::Octal(1) => 4,  // " 377"
88        OutputFormat::Octal(2) => 7,  // " 177777"
89        OutputFormat::Octal(4) => 12, // " 37777777777"
90        OutputFormat::Octal(8) => 23, // " 1777777777777777777777"
91        OutputFormat::Hex(1) => 3,    // " ff"
92        OutputFormat::Hex(2) => 5,    // " ffff"
93        OutputFormat::Hex(4) => 9,    // " ffffffff"
94        OutputFormat::Hex(8) => 17,   // " ffffffffffffffff"
95        OutputFormat::UnsignedDec(1) => 4, // " 255"
96        OutputFormat::UnsignedDec(2) => 6, // " 65535"
97        OutputFormat::UnsignedDec(4) => 11, // " 4294967295"
98        OutputFormat::UnsignedDec(8) => 21, // " 18446744073709551615"
99        OutputFormat::SignedDec(1) => 5, // " -128"
100        OutputFormat::SignedDec(2) => 7, // " -32768"
101        OutputFormat::SignedDec(4) => 12, // " -2147483648"
102        OutputFormat::SignedDec(8) => 21, // " -9223372036854775808"
103        OutputFormat::Float(4) => 16, // "   x.xxxxxxxe+xx" (3 leading spaces for positive max)
104        OutputFormat::Float(8) => 25, // " -x.xxxxxxxxxxxxxxe+xxx"
105        _ => 4,
106    }
107}
108
109/// Get the byte size of a format element.
110fn element_size(fmt: OutputFormat) -> usize {
111    match fmt {
112        OutputFormat::NamedChar | OutputFormat::PrintableChar => 1,
113        OutputFormat::SignedDec(s)
114        | OutputFormat::Float(s)
115        | OutputFormat::Octal(s)
116        | OutputFormat::UnsignedDec(s)
117        | OutputFormat::Hex(s) => s,
118    }
119}
120
121/// Format a float using C's %g format.
122/// Uses libc snprintf on Unix and Rust formatting on Windows.
123fn snprintf_g(v: f64, precision: usize) -> String {
124    let precision = precision.min(50);
125    #[cfg(unix)]
126    {
127        // Pre-built format strings for common precisions to avoid allocation
128        static FMT_STRINGS: &[&std::ffi::CStr] = &[
129            c"%.0g", c"%.1g", c"%.2g", c"%.3g", c"%.4g", c"%.5g", c"%.6g", c"%.7g", c"%.8g",
130            c"%.9g", c"%.10g", c"%.11g", c"%.12g", c"%.13g", c"%.14g", c"%.15g", c"%.16g",
131            c"%.17g", c"%.18g", c"%.19g", c"%.20g",
132        ];
133        let mut buf = [0u8; 64];
134        let fmt_cstr: std::ffi::CString;
135        let fmt_ptr = if precision < FMT_STRINGS.len() {
136            FMT_STRINGS[precision].as_ptr()
137        } else {
138            fmt_cstr = std::ffi::CString::new(format!("%.{}g", precision)).unwrap();
139            fmt_cstr.as_ptr()
140        };
141        let len =
142            unsafe { libc::snprintf(buf.as_mut_ptr() as *mut libc::c_char, buf.len(), fmt_ptr, v) };
143        if len > 0 && (len as usize) < buf.len() {
144            return String::from_utf8_lossy(&buf[..len as usize]).into_owned();
145        }
146    }
147    // Fallback / Windows: use Rust formatting with %g-like behavior
148    let s = format!("{:.prec$e}", v, prec = precision.saturating_sub(1));
149    // Convert scientific notation to shortest form like %g
150    if let Some(e_pos) = s.find('e') {
151        let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0);
152        if exp >= -(precision as i32) && exp < precision as i32 {
153            // Use fixed notation
154            let fixed = format!(
155                "{:.prec$}",
156                v,
157                prec = (precision as i32 - 1 - exp).max(0) as usize
158            );
159            // Trim trailing zeros after decimal point
160            if fixed.contains('.') {
161                let trimmed = fixed.trim_end_matches('0').trim_end_matches('.');
162                return trimmed.to_string();
163            }
164            return fixed;
165        }
166    }
167    format!("{:.*e}", precision.saturating_sub(1), v)
168}
169
170/// Format f32 like GNU od: uses %.8g formatting (8 significant digits).
171fn format_float_f32(v: f32) -> String {
172    // Use shortest decimal representation that uniquely round-trips (like Ryu / GNU od).
173    // Try increasing precisions from FLT_DIG (6) to FLT_DECIMAL_DIG (9).
174    for prec in 6usize..=9 {
175        let s = snprintf_g(v as f64, prec);
176        if let Ok(reparsed) = s.trim().parse::<f32>() {
177            if reparsed == v {
178                return s;
179            }
180        }
181    }
182    snprintf_g(v as f64, 9)
183}
184
185/// Format f64 like GNU od: shortest representation that round-trips.
186/// Try increasing precisions from DBL_DIG (15) to DBL_DECIMAL_DIG (17).
187fn format_float_f64(v: f64) -> String {
188    for prec in 15usize..=17 {
189        let s = snprintf_g(v, prec);
190        if let Ok(reparsed) = s.trim().parse::<f64>() {
191            if reparsed.to_bits() == v.to_bits() {
192                return s;
193            }
194        }
195    }
196    snprintf_g(v, 17)
197}
198
199/// Read a u16 from bytes with the specified endianness.
200#[inline]
201fn read_u16(bytes: &[u8], endian: Endian) -> u16 {
202    let arr: [u8; 2] = bytes[..2].try_into().unwrap();
203    match endian {
204        Endian::Big => u16::from_be_bytes(arr),
205        Endian::Little | Endian::Native => u16::from_le_bytes(arr),
206    }
207}
208
209/// Read a u32 from bytes with the specified endianness.
210#[inline]
211fn read_u32(bytes: &[u8], endian: Endian) -> u32 {
212    let arr: [u8; 4] = bytes[..4].try_into().unwrap();
213    match endian {
214        Endian::Big => u32::from_be_bytes(arr),
215        Endian::Little | Endian::Native => u32::from_le_bytes(arr),
216    }
217}
218
219/// Read a u64 from bytes with the specified endianness.
220#[inline]
221fn read_u64(bytes: &[u8], endian: Endian) -> u64 {
222    let arr: [u8; 8] = bytes[..8].try_into().unwrap();
223    match endian {
224        Endian::Big => u64::from_be_bytes(arr),
225        Endian::Little | Endian::Native => u64::from_le_bytes(arr),
226    }
227}
228
229/// Hex digit table.
230static HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
231
232/// Format a u64 as zero-padded octal into a stack buffer.
233/// Returns the number of bytes written (always `digits`).
234#[inline]
235fn fmt_octal(mut v: u64, buf: &mut [u8], digits: usize) -> usize {
236    let mut i = digits;
237    while i > 0 {
238        i -= 1;
239        buf[i] = b'0' + (v & 7) as u8;
240        v >>= 3;
241    }
242    digits
243}
244
245/// Format a u64 as zero-padded hex into a stack buffer.
246/// Returns the number of bytes written (always `digits`).
247#[inline]
248fn fmt_hex(mut v: u64, buf: &mut [u8], digits: usize) -> usize {
249    let mut i = digits;
250    while i > 0 {
251        i -= 1;
252        buf[i] = HEX_DIGITS[(v & 0xF) as usize];
253        v >>= 4;
254    }
255    digits
256}
257
258/// Format a u64 as decimal into a stack buffer (right-aligned, no padding).
259/// Returns the number of bytes written.
260#[inline]
261fn fmt_unsigned(mut v: u64, buf: &mut [u8]) -> usize {
262    if v == 0 {
263        buf[0] = b'0';
264        return 1;
265    }
266    let mut i = 0;
267    while v > 0 {
268        buf[i] = b'0' + (v % 10) as u8;
269        v /= 10;
270        i += 1;
271    }
272    // Reverse in-place
273    buf[..i].reverse();
274    i
275}
276
277/// Format an i64 as decimal into a stack buffer.
278/// Returns the number of bytes written.
279#[inline]
280fn fmt_signed(v: i64, buf: &mut [u8]) -> usize {
281    if v >= 0 {
282        return fmt_unsigned(v as u64, buf);
283    }
284    buf[0] = b'-';
285    let len = fmt_unsigned((-(v as i128)) as u64, &mut buf[1..]);
286    1 + len
287}
288
289/// Write a left-padded (right-aligned) value to output. `value_buf[..value_len]` contains the
290/// formatted number, and `width` is the total field width (including leading spaces).
291#[inline]
292fn write_padded(
293    out: &mut impl Write,
294    value_buf: &[u8],
295    value_len: usize,
296    width: usize,
297) -> io::Result<()> {
298    const SPACES: [u8; 32] = [b' '; 32];
299    let pad = width.saturating_sub(value_len);
300    let mut remaining = pad;
301    while remaining > 0 {
302        let chunk = remaining.min(SPACES.len());
303        out.write_all(&SPACES[..chunk])?;
304        remaining -= chunk;
305    }
306    out.write_all(&value_buf[..value_len])
307}
308
309/// Write a formatted value directly to the output using stack buffers (zero heap allocation).
310#[inline]
311fn write_value(
312    out: &mut impl Write,
313    bytes: &[u8],
314    fmt: OutputFormat,
315    width: usize,
316    endian: Endian,
317) -> io::Result<()> {
318    let mut buf = [0u8; 24]; // max: 22 octal digits for u64 + 2 spare
319    match fmt {
320        OutputFormat::NamedChar => {
321            let b = bytes[0];
322            if b < 128 {
323                let s = NAMED_CHARS[b as usize].as_bytes();
324                write_padded(out, s, s.len(), width)
325            } else {
326                let len = fmt_octal(b as u64, &mut buf, 3);
327                write_padded(out, &buf, len, width)
328            }
329        }
330        OutputFormat::PrintableChar => {
331            let b = bytes[0];
332            let s: &[u8] = match b {
333                0x00 => b"\\0",
334                0x07 => b"\\a",
335                0x08 => b"\\b",
336                0x09 => b"\\t",
337                0x0a => b"\\n",
338                0x0b => b"\\v",
339                0x0c => b"\\f",
340                0x0d => b"\\r",
341                _ => b"",
342            };
343            if !s.is_empty() {
344                write_padded(out, s, s.len(), width)
345            } else if (0x20..=0x7e).contains(&b) {
346                buf[0] = b;
347                write_padded(out, &buf, 1, width)
348            } else {
349                buf[0] = b'0' + (b >> 6);
350                buf[1] = b'0' + ((b >> 3) & 7);
351                buf[2] = b'0' + (b & 7);
352                write_padded(out, &buf, 3, width)
353            }
354        }
355        OutputFormat::Octal(size) => {
356            let (v, digits) = match size {
357                1 => (bytes[0] as u64, 3),
358                2 => (read_u16(bytes, endian) as u64, 6),
359                4 => (read_u32(bytes, endian) as u64, 11),
360                8 => (read_u64(bytes, endian), 22),
361                _ => return Ok(()),
362            };
363            let len = fmt_octal(v, &mut buf, digits);
364            write_padded(out, &buf, len, width)
365        }
366        OutputFormat::Hex(size) => {
367            let (v, digits) = match size {
368                1 => (bytes[0] as u64, 2),
369                2 => (read_u16(bytes, endian) as u64, 4),
370                4 => (read_u32(bytes, endian) as u64, 8),
371                8 => (read_u64(bytes, endian), 16),
372                _ => return Ok(()),
373            };
374            let len = fmt_hex(v, &mut buf, digits);
375            write_padded(out, &buf, len, width)
376        }
377        OutputFormat::UnsignedDec(size) => {
378            let v = match size {
379                1 => bytes[0] as u64,
380                2 => read_u16(bytes, endian) as u64,
381                4 => read_u32(bytes, endian) as u64,
382                8 => read_u64(bytes, endian),
383                _ => return Ok(()),
384            };
385            let len = fmt_unsigned(v, &mut buf);
386            write_padded(out, &buf, len, width)
387        }
388        OutputFormat::SignedDec(size) => {
389            let v: i64 = match size {
390                1 => bytes[0] as i8 as i64,
391                2 => read_u16(bytes, endian) as i16 as i64,
392                4 => read_u32(bytes, endian) as i32 as i64,
393                8 => read_u64(bytes, endian) as i64,
394                _ => return Ok(()),
395            };
396            let len = fmt_signed(v, &mut buf);
397            write_padded(out, &buf, len, width)
398        }
399        OutputFormat::Float(size) => match size {
400            4 => {
401                let v = f32::from_bits(read_u32(bytes, endian));
402                write!(out, "{:>w$}", format_float_f32(v), w = width)
403            }
404            8 => {
405                let v = f64::from_bits(read_u64(bytes, endian));
406                write!(out, "{:>w$}", format_float_f64(v), w = width)
407            }
408            _ => Ok(()),
409        },
410    }
411}
412
413/// Compute the effective field width for each format, ensuring multi-format alignment.
414/// GNU od computes the total chars_per_block for each format (num_elements * field_width),
415/// takes the maximum across all formats, then distributes that evenly back to each format.
416fn compute_effective_widths(formats: &[OutputFormat], line_width: usize) -> Vec<usize> {
417    if formats.len() <= 1 {
418        return formats.iter().map(|f| field_width(*f)).collect();
419    }
420
421    let mut max_chars_per_block = 0usize;
422    for fmt in formats {
423        let es = element_size(*fmt);
424        let fw = field_width(*fmt);
425        let num_elems = line_width / es;
426        let chars = num_elems * fw;
427        if chars > max_chars_per_block {
428            max_chars_per_block = chars;
429        }
430    }
431
432    // Now compute effective field width for each format
433    formats
434        .iter()
435        .map(|fmt| {
436            let es = element_size(*fmt);
437            let num_elems = line_width / es;
438            if num_elems > 0 {
439                max_chars_per_block / num_elems
440            } else {
441                field_width(*fmt)
442            }
443        })
444        .collect()
445}
446
447/// Write one line of output for a given format type directly to the writer.
448fn write_format_line(
449    out: &mut impl Write,
450    chunk: &[u8],
451    fmt: OutputFormat,
452    line_width: usize,
453    is_first_format: bool,
454    radix: AddressRadix,
455    offset: u64,
456    z_annotate: bool,
457    effective_fw: usize,
458    endian: Endian,
459) -> io::Result<()> {
460    // Address prefix — use stack buffer to avoid format!() allocation
461    if is_first_format {
462        let mut addr_buf = [0u8; 22];
463        match radix {
464            AddressRadix::Octal => {
465                let len = fmt_octal(offset, &mut addr_buf, 7);
466                out.write_all(&addr_buf[..len])?;
467            }
468            AddressRadix::Decimal => {
469                let mut tmp = [0u8; 20];
470                let vlen = fmt_unsigned(offset, &mut tmp);
471                // Zero-pad to 7 digits
472                let pad = 7usize.saturating_sub(vlen);
473                for b in addr_buf.iter_mut().take(pad) {
474                    *b = b'0';
475                }
476                addr_buf[pad..pad + vlen].copy_from_slice(&tmp[..vlen]);
477                out.write_all(&addr_buf[..pad + vlen])?;
478            }
479            AddressRadix::Hex => {
480                let len = fmt_hex(offset, &mut addr_buf, 6);
481                out.write_all(&addr_buf[..len])?;
482            }
483            AddressRadix::None => {}
484        }
485    } else if radix != AddressRadix::None {
486        let addr_width = match radix {
487            AddressRadix::Octal | AddressRadix::Decimal => 7,
488            AddressRadix::Hex => 6,
489            AddressRadix::None => 0,
490        };
491        for _ in 0..addr_width {
492            out.write_all(b" ")?;
493        }
494    }
495
496    let elem_sz = element_size(fmt);
497    let fw = effective_fw;
498    let num_elems = line_width / elem_sz;
499    let actual_full = chunk.len() / elem_sz;
500    let remainder = chunk.len() % elem_sz;
501
502    for i in 0..num_elems {
503        if i < actual_full {
504            let start = i * elem_sz;
505            let end = start + elem_sz;
506            write_value(out, &chunk[start..end], fmt, fw, endian)?;
507        } else if i == actual_full && remainder > 0 {
508            let start = i * elem_sz;
509            let mut padded = [0u8; 8]; // max element size is 8
510            padded[..remainder].copy_from_slice(&chunk[start..]);
511            write_value(out, &padded[..elem_sz], fmt, fw, endian)?;
512        }
513    }
514
515    // Append printable ASCII annotation if 'z' suffix was used
516    if z_annotate {
517        // Pad remaining columns to align the annotation
518        let used_cols = actual_full + if remainder > 0 { 1 } else { 0 };
519        for _ in used_cols..num_elems {
520            for _ in 0..fw {
521                out.write_all(b" ")?;
522            }
523        }
524        out.write_all(b"  >")?;
525        for &b in chunk {
526            if b.is_ascii_graphic() || b == b' ' {
527                out.write_all(&[b])?;
528            } else {
529                out.write_all(b".")?;
530            }
531        }
532        out.write_all(b"<")?;
533    }
534
535    writeln!(out)?;
536    Ok(())
537}
538
539/// Parse a format type string (the TYPE argument of -t).
540/// Returns the format and whether the 'z' suffix was present.
541pub fn parse_format_type(s: &str) -> Result<(OutputFormat, bool), String> {
542    if s.is_empty() {
543        return Err("empty format string".to_string());
544    }
545
546    // Strip trailing 'z' suffix (printable ASCII annotation)
547    let (s, z_annotate) = if s.len() > 1 && s.ends_with('z') {
548        (&s[..s.len() - 1], true)
549    } else {
550        (s, false)
551    };
552
553    let mut chars = s.chars();
554    let type_char = chars.next().unwrap();
555    let size_str: String = chars.collect();
556
557    let fmt = match type_char {
558        'a' => Ok(OutputFormat::NamedChar),
559        'c' => Ok(OutputFormat::PrintableChar),
560        'd' => {
561            let size = if size_str.is_empty() {
562                4
563            } else {
564                parse_size_spec(&size_str, "d")?
565            };
566            Ok(OutputFormat::SignedDec(size))
567        }
568        'f' => {
569            let size = if size_str.is_empty() {
570                4
571            } else {
572                parse_float_size(&size_str)?
573            };
574            Ok(OutputFormat::Float(size))
575        }
576        'o' => {
577            let size = if size_str.is_empty() {
578                2
579            } else {
580                parse_size_spec(&size_str, "o")?
581            };
582            Ok(OutputFormat::Octal(size))
583        }
584        'u' => {
585            let size = if size_str.is_empty() {
586                4
587            } else {
588                parse_size_spec(&size_str, "u")?
589            };
590            Ok(OutputFormat::UnsignedDec(size))
591        }
592        'x' => {
593            let size = if size_str.is_empty() {
594                2
595            } else {
596                parse_size_spec(&size_str, "x")?
597            };
598            Ok(OutputFormat::Hex(size))
599        }
600        _ => Err(format!("invalid type string '{}'", s)),
601    }?;
602    Ok((fmt, z_annotate))
603}
604
605fn parse_size_spec(s: &str, type_name: &str) -> Result<usize, String> {
606    // Accept C, S, I, L or a number
607    match s {
608        "C" => Ok(1),
609        "S" => Ok(2),
610        "I" => Ok(4),
611        "L" => Ok(8),
612        _ => {
613            let n: usize = s
614                .parse()
615                .map_err(|_| format!("invalid type string '{}{}': invalid size", type_name, s))?;
616            match n {
617                1 | 2 | 4 | 8 => Ok(n),
618                _ => Err(format!(
619                    "invalid type string '{}{}': invalid size",
620                    type_name, s
621                )),
622            }
623        }
624    }
625}
626
627fn parse_float_size(s: &str) -> Result<usize, String> {
628    match s {
629        "F" | "4" => Ok(4),
630        "D" | "8" => Ok(8),
631        "L" | "16" => Err("16-byte float not supported".to_string()),
632        _ => {
633            let n: usize = s
634                .parse()
635                .map_err(|_| format!("invalid float size '{}'", s))?;
636            match n {
637                4 | 8 => Ok(n),
638                _ => Err(format!("invalid float size '{}'", s)),
639            }
640        }
641    }
642}
643
644/// Process input and produce od output.
645pub fn od_process<R: Read, W: Write>(
646    mut input: R,
647    output: &mut W,
648    config: &OdConfig,
649) -> io::Result<()> {
650    // Skip bytes
651    if config.skip_bytes > 0 {
652        let mut to_skip = config.skip_bytes;
653        let mut skip_buf = [0u8; 8192];
654        while to_skip > 0 {
655            let chunk_size = std::cmp::min(to_skip, skip_buf.len() as u64) as usize;
656            let n = input.read(&mut skip_buf[..chunk_size])?;
657            if n == 0 {
658                break;
659            }
660            to_skip -= n as u64;
661        }
662    }
663
664    // Read all data (respecting read_bytes limit)
665    let data = match config.read_bytes {
666        Some(limit) => {
667            let mut buf = Vec::new();
668            let mut limited = input.take(limit);
669            limited.read_to_end(&mut buf)?;
670            buf
671        }
672        None => {
673            let mut buf = Vec::new();
674            input.read_to_end(&mut buf)?;
675            buf
676        }
677    };
678
679    let width = config.width;
680    let mut offset = config.skip_bytes;
681    let mut prev_chunk: Option<Vec<u8>> = None;
682    let mut star_printed = false;
683
684    // Compute effective field widths for multi-format alignment
685    let effective_widths = compute_effective_widths(&config.formats, width);
686
687    let mut pos = 0;
688    while pos < data.len() {
689        let end = std::cmp::min(pos + width, data.len());
690        let chunk = &data[pos..end];
691
692        // Duplicate suppression
693        if !config.show_duplicates && chunk.len() == width {
694            if let Some(ref prev) = prev_chunk {
695                if prev.as_slice() == chunk {
696                    if !star_printed {
697                        writeln!(output, "*")?;
698                        star_printed = true;
699                    }
700                    pos += width;
701                    offset += width as u64;
702                    continue;
703                }
704            }
705        }
706
707        star_printed = false;
708
709        for (i, fmt) in config.formats.iter().enumerate() {
710            let z = config.z_flags.get(i).copied().unwrap_or(false);
711            let ew = effective_widths[i];
712            write_format_line(
713                output,
714                chunk,
715                *fmt,
716                width,
717                i == 0,
718                config.address_radix,
719                offset,
720                z,
721                ew,
722                config.endian,
723            )?;
724        }
725
726        prev_chunk = Some(chunk.to_vec());
727        pos += width;
728        offset += width as u64;
729    }
730
731    // Final address line
732    if config.address_radix != AddressRadix::None {
733        let final_offset = config.skip_bytes + data.len() as u64;
734        match config.address_radix {
735            AddressRadix::Octal => writeln!(output, "{:07o}", final_offset)?,
736            AddressRadix::Decimal => writeln!(output, "{:07}", final_offset)?,
737            AddressRadix::Hex => writeln!(output, "{:06x}", final_offset)?,
738            AddressRadix::None => {}
739        }
740    }
741
742    Ok(())
743}