Merge pull request #8849 from sylvestre/od-n

od: add support for -S
This commit is contained in:
Daniel Hofstetter
2025-10-17 10:03:28 +02:00
committed by GitHub
4 changed files with 281 additions and 70 deletions
+15
View File
@@ -66,6 +66,7 @@ od-help-address-radix = Select the base in which file offsets are printed.
od-help-skip-bytes = Skip bytes input bytes before formatting and writing.
od-help-read-bytes = limit dump to BYTES input bytes
od-help-endian = byte order to use for multi-byte formats
od-help-strings = output strings of at least BYTES graphic chars. 3 is assumed when BYTES is not specified.
od-help-a = named characters, ignoring high-order bit
od-help-b = octal bytes
od-help-c = ASCII characters or backslash escapes
@@ -76,3 +77,17 @@ od-help-output-duplicates = do not use * to mark line suppression
od-help-width = output BYTES bytes per output line. 32 is implied when BYTES is not
specified.
od-help-traditional = compatibility mode with one input, offset and label.
od-help-o = octal 2-byte units
od-help-capital-i = decimal 8-byte units
od-help-capital-l = decimal 8-byte units
od-help-i = decimal 4-byte units
od-help-l = decimal 8-byte units
od-help-x = hexadecimal 2-byte units
od-help-h = hexadecimal 2-byte units
od-help-capital-o = octal 4-byte units
od-help-s = decimal 2-byte units
od-help-capital-x = hexadecimal 4-byte units
od-help-capital-h = hexadecimal 4-byte units
od-help-e = floating point double precision (64-bit) units
od-help-f = floating point double precision (32-bit) units
od-help-capital-f = floating point double precision (64-bit) units
+15
View File
@@ -67,6 +67,7 @@ od-help-address-radix = Sélectionner la base dans laquelle les décalages de fi
od-help-skip-bytes = Ignorer les octets d'entrée avant le formatage et l'écriture.
od-help-read-bytes = limiter le dump à OCTETS octets d'entrée
od-help-endian = ordre des octets à utiliser pour les formats multi-octets
od-help-strings = afficher les chaînes d'au moins OCTETS caractères graphiques. 3 est supposé quand OCTETS n'est pas spécifié.
od-help-a = caractères nommés, ignorant le bit d'ordre supérieur
od-help-b = octets octaux
od-help-c = caractères ASCII ou échappements antislash
@@ -77,3 +78,17 @@ od-help-output-duplicates = ne pas utiliser * pour marquer la suppression de lig
od-help-width = sortir OCTETS octets par ligne de sortie. 32 est impliqué quand OCTETS n'est pas
spécifié.
od-help-traditional = mode de compatibilité avec une entrée, décalage et étiquette.
od-help-o = unités octales 2-octets
od-help-capital-i = unités décimales 8-octets
od-help-capital-l = unités décimales 8-octets
od-help-i = unités décimales 4-octets
od-help-l = unités décimales 8-octets
od-help-x = unités hexadécimales 2-octets
od-help-h = unités hexadécimales 2-octets
od-help-capital-o = unités octales 4-octets
od-help-s = unités décimales 2-octets
od-help-capital-x = unités hexadécimales 4-octets
od-help-capital-h = unités hexadécimales 4-octets
od-help-e = unités virgule flottante double précision (64-bits)
od-help-f = unités virgule flottante double précision (32-bits)
od-help-capital-f = unités virgule flottante double précision (64-bits)
+186 -70
View File
@@ -26,7 +26,7 @@ mod prn_int;
use std::cmp;
use std::fmt::Write;
use std::io::BufReader;
use std::io::{BufReader, Read};
use crate::byteorder_io::ByteOrder;
use crate::formatter_item_info::FormatWriter;
@@ -76,6 +76,21 @@ struct OdOptions {
line_bytes: usize,
output_duplicates: bool,
radix: Radix,
string_min_length: Option<usize>,
}
/// Helper function to parse bytes with error handling
fn parse_bytes_option(matches: &ArgMatches, option_name: &str) -> UResult<Option<u64>> {
match matches.get_one::<String>(option_name) {
None => Ok(None),
Some(s) => match parse_number_of_bytes(s) {
Ok(n) => Ok(Some(n)),
Err(e) => Err(USimpleError::new(
1,
format_error_message(&e, s, option_name),
)),
},
}
}
impl OdOptions {
@@ -95,18 +110,7 @@ impl OdOptions {
ByteOrder::Native
};
let mut skip_bytes = match matches.get_one::<String>(options::SKIP_BYTES) {
None => 0,
Some(s) => match parse_number_of_bytes(s) {
Ok(n) => n,
Err(e) => {
return Err(USimpleError::new(
1,
format_error_message(&e, s, options::SKIP_BYTES),
));
}
},
};
let mut skip_bytes = parse_bytes_option(matches, options::SKIP_BYTES)?.unwrap_or(0);
let mut label: Option<u64> = None;
@@ -156,17 +160,16 @@ impl OdOptions {
let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES);
let read_bytes = match matches.get_one::<String>(options::READ_BYTES) {
let read_bytes = parse_bytes_option(matches, options::READ_BYTES)?;
let string_min_length = match parse_bytes_option(matches, options::STRINGS)? {
None => None,
Some(s) => match parse_number_of_bytes(s) {
Ok(n) => Some(n),
Err(e) => {
return Err(USimpleError::new(
1,
format_error_message(&e, s, options::READ_BYTES),
));
}
},
Some(n) => Some(usize::try_from(n).map_err(|_| {
USimpleError::new(
1,
translate!("od-error-argument-too-large", "option" => "-S", "value" => n.to_string()),
)
})?),
};
let radix = match matches.get_one::<String>(options::ADDRESS_RADIX) {
@@ -208,6 +211,7 @@ impl OdOptions {
line_bytes,
output_duplicates,
radix,
string_min_length,
})
}
}
@@ -224,28 +228,39 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let od_options = OdOptions::new(&clap_matches, &args)?;
let mut input_offset =
InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label);
// Check if we're in strings mode
if let Some(min_length) = od_options.string_min_length {
extract_strings_from_input(
&od_options.input_strings,
od_options.skip_bytes,
od_options.read_bytes,
min_length,
od_options.radix,
)
} else {
let mut input_offset =
InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label);
let mut input = open_input_peek_reader(
&od_options.input_strings,
od_options.skip_bytes,
od_options.read_bytes,
);
let mut input_decoder = InputDecoder::new(
&mut input,
od_options.line_bytes,
PEEK_BUFFER_SIZE,
od_options.byte_order,
);
let mut input = open_input_peek_reader(
&od_options.input_strings,
od_options.skip_bytes,
od_options.read_bytes,
);
let mut input_decoder = InputDecoder::new(
&mut input,
od_options.line_bytes,
PEEK_BUFFER_SIZE,
od_options.byte_order,
);
let output_info = OutputInfo::new(
od_options.line_bytes,
&od_options.formats[..],
od_options.output_duplicates,
);
let output_info = OutputInfo::new(
od_options.line_bytes,
&od_options.formats[..],
od_options.output_duplicates,
);
odfunc(&mut input_offset, &mut input_decoder, &output_info)
odfunc(&mut input_offset, &mut input_decoder, &output_info)
}
}
pub fn uu_app() -> Command {
@@ -263,7 +278,7 @@ pub fn uu_app() -> Command {
Arg::new(options::HELP)
.long(options::HELP)
.help(translate!("od-help-help"))
.action(ArgAction::Help)
.action(ArgAction::Help),
)
.arg(
Arg::new(options::ADDRESS_RADIX)
@@ -297,10 +312,8 @@ pub fn uu_app() -> Command {
Arg::new(options::STRINGS)
.short('S')
.long(options::STRINGS)
.help(
"NotImplemented: output strings of at least BYTES graphic chars. 3 is assumed when \
BYTES is not specified.",
)
.help(translate!("od-help-strings"))
.num_args(0..=1)
.default_missing_value("3")
.value_name("BYTES"),
)
@@ -337,85 +350,85 @@ pub fn uu_app() -> Command {
.arg(
Arg::new("o")
.short('o')
.help("octal 2-byte units")
.help(translate!("od-help-o"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("I")
.short('I')
.help("decimal 8-byte units")
.help(translate!("od-help-capital-i"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("L")
.short('L')
.help("decimal 8-byte units")
.help(translate!("od-help-capital-l"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("i")
.short('i')
.help("decimal 4-byte units")
.help(translate!("od-help-i"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("l")
.short('l')
.help("decimal 8-byte units")
.help(translate!("od-help-l"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("x")
.short('x')
.help("hexadecimal 2-byte units")
.help(translate!("od-help-x"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("h")
.short('h')
.help("hexadecimal 2-byte units")
.help(translate!("od-help-h"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("O")
.short('O')
.help("octal 4-byte units")
.action(ArgAction::SetTrue)
.help(translate!("od-help-capital-o"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("s")
.short('s')
.help("decimal 2-byte units")
.help(translate!("od-help-s"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("X")
.short('X')
.help("hexadecimal 4-byte units")
.help(translate!("od-help-capital-x"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("H")
.short('H')
.help("hexadecimal 4-byte units")
.help(translate!("od-help-capital-h"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("e")
.short('e')
.help("floating point double precision (64-bit) units")
.help(translate!("od-help-e"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("f")
.short('f')
.help("floating point double precision (32-bit) units")
.help(translate!("od-help-f"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("F")
.short('F')
.help("floating point double precision (64-bit) units")
.help(translate!("od-help-capital-f"))
.action(ArgAction::SetTrue),
)
.arg(
@@ -532,6 +545,105 @@ where
}
}
/// Extract and display printable strings from input (od -S option)
fn extract_strings_from_input(
input_strings: &[String],
skip_bytes: u64,
read_bytes: Option<u64>,
min_length: usize,
radix: Radix,
) -> UResult<()> {
let inputs = map_input_strings(input_strings);
let mut mf = MultifileReader::new(inputs);
// Apply skip_bytes by reading and discarding
let mut skipped = 0u64;
while skipped < skip_bytes {
let to_skip = std::cmp::min(8192, skip_bytes - skipped);
let mut skip_buf = vec![0u8; to_skip as usize];
match mf.read(&mut skip_buf) {
Ok(0) => break, // EOF reached
Ok(n) => skipped += n as u64,
Err(_) => break,
}
}
// Helper function to format and print a string
let print_string = |offset: u64, string: &[u8]| {
let string_content = String::from_utf8_lossy(string);
match radix {
Radix::NoPrefix => println!("{string_content}"),
Radix::Decimal => println!("{offset:07} {string_content}"),
Radix::Hexadecimal => println!("{offset:07x} {string_content}"),
Radix::Octal => println!("{offset:07o} {string_content}"),
}
};
let mut current_string = Vec::new();
let mut string_start_offset = 0u64;
let mut current_offset = skip_bytes;
let mut bytes_read = 0u64;
let mut buf = [0u8; 1];
loop {
// Check if we've reached the read_bytes limit
if let Some(limit) = read_bytes {
if bytes_read >= limit {
// Special case: when -N limit is reached with a pending string
// that meets min_length, output it even without null terminator
if current_string.len() >= min_length {
print_string(string_start_offset, &current_string);
}
break;
}
}
// Read one byte at a time
match mf.read(&mut buf) {
Ok(0) => break, // EOF
Ok(_) => {
bytes_read += 1;
let byte = buf[0];
// Check if it's a printable character (including space)
if (0x20..=0x7E).contains(&byte) {
if current_string.is_empty() {
string_start_offset = current_offset;
}
current_string.push(byte);
} else {
// Either null terminator or non-printable character
if byte == 0 && current_string.len() >= min_length {
// Null terminator found with valid string
print_string(string_start_offset, &current_string);
}
current_string.clear();
}
current_offset += 1;
}
Err(e) => {
// Note: GNU od does not output unterminated strings at EOF
// Strings must be null-terminated to be output
if mf.has_error() {
show_error!("{}", e);
return Err(1.into());
}
break;
}
}
}
// GNU od doesn't output an offset when strings mode finds no valid strings
// This includes cases with only unterminated or too-short strings
if mf.has_error() {
Err(1.into())
} else {
Ok(())
}
}
/// Outputs a single line of input, into one or more lines human readable output.
fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &OutputInfo) {
let mut first = true; // First line of a multi-format raster.
@@ -595,6 +707,17 @@ fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &Output
}
}
/// Helper function to convert input strings to InputSource
fn map_input_strings(input_strings: &[String]) -> Vec<InputSource<'_>> {
input_strings
.iter()
.map(|w| match w as &str {
"-" => InputSource::Stdin,
x => InputSource::FileName(x),
})
.collect()
}
/// returns a reader implementing `PeekRead + Read + HasError` providing the combined input
///
/// `skip_bytes` is the number of bytes skipped from the input
@@ -605,14 +728,7 @@ fn open_input_peek_reader(
read_bytes: Option<u64>,
) -> PeekReader<BufReader<PartialReader<MultifileReader<'_>>>> {
// should return "impl PeekRead + Read + HasError" when supported in (stable) rust
let inputs = input_strings
.iter()
.map(|w| match w as &str {
"-" => InputSource::Stdin,
x => InputSource::FileName(x),
})
.collect::<Vec<_>>();
let inputs = map_input_strings(input_strings);
let mf = MultifileReader::new(inputs);
let pr = PartialReader::new(mf, skip_bytes, read_bytes);
// Add a BufReader over the top of the PartialReader. This will have the
+65
View File
@@ -992,3 +992,68 @@ fn test_od_options_after_filename() {
.no_stderr()
.stdout_is(" 1c68 fdbb\n");
}
#[test]
fn test_od_strings_option() {
// Test -S option: output strings of at least N graphic chars
// Test -S0: output all null-terminated strings regardless of length
new_ucmd!()
.arg("-S0")
.pipe_in(b"hello\x00world\x00")
.succeeds()
.stdout_is("0000000 hello\n0000006 world\n");
// Test -S0 with single character strings
new_ucmd!()
.arg("-S0")
.pipe_in(b"a\x00b\x00cd\x00")
.succeeds()
.stdout_is("0000000 a\n0000002 b\n0000004 cd\n");
// Test with null-terminated strings
new_ucmd!()
.arg("-S3")
.pipe_in(b"\x01hello\x00world\x00ab\x00")
.succeeds()
.stdout_is("0000001 hello\n0000007 world\n");
// Test with -S10 to show only strings with minimum length
new_ucmd!()
.arg("-S10")
.pipe_in(b"\x01 \x00 \x00")
.succeeds()
.stdout_is("0000001 \n0000014 \n");
// Test with unterminated string at EOF (should not output)
new_ucmd!()
.arg("-S10")
.pipe_in(b" ")
.succeeds()
.stdout_is("");
// Test with -N limit and pending string (should output even without null)
let expected = "0000000 \n";
new_ucmd!()
.arg("-N2")
.arg("-S1")
.pipe_in(" ")
.succeeds()
.stdout_is(expected);
// Test with -N limit and string too short
new_ucmd!()
.arg("-N11")
.arg("-S11")
.pipe_in(b" \x00")
.succeeds()
.stdout_is("");
// Test with no address prefix (-An)
new_ucmd!()
.arg("-S3")
.arg("-An")
.pipe_in(b"hello\x00world\x00")
.succeeds()
.stdout_is("hello\nworld\n");
}