diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl index 38bf1c577..208bd3333 100644 --- a/src/uu/od/locales/en-US.ftl +++ b/src/uu/od/locales/en-US.ftl @@ -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 diff --git a/src/uu/od/locales/fr-FR.ftl b/src/uu/od/locales/fr-FR.ftl index 712550667..cba433b64 100644 --- a/src/uu/od/locales/fr-FR.ftl +++ b/src/uu/od/locales/fr-FR.ftl @@ -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) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index e4a8ce108..80e6893d1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -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, +} + +/// Helper function to parse bytes with error handling +fn parse_bytes_option(matches: &ArgMatches, option_name: &str) -> UResult> { + match matches.get_one::(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::(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 = None; @@ -156,17 +160,16 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = match matches.get_one::(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::(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, + 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, ¤t_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, ¤t_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> { + 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, ) -> PeekReader>>> { // 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::>(); - + 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 diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 4ee31823e..83c864b8e 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -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"); +}