diff --git a/src/uu/hexdump/src/hexdump.rs b/src/uu/hexdump/src/hexdump.rs index 115d036d..f2861049 100644 --- a/src/uu/hexdump/src/hexdump.rs +++ b/src/uu/hexdump/src/hexdump.rs @@ -16,6 +16,8 @@ use uucore::{ struct ChainedFileReader { file_paths: Vec, + /// No file was given, so the input is stdin + reads_stdin: bool, current_file: Option>, current_file_index: usize, remaining_bytes: Option, @@ -23,9 +25,11 @@ struct ChainedFileReader { } impl ChainedFileReader { - fn new(file_paths: Vec, length_limit: Option) -> Self { + fn new(file_paths: Option>, length_limit: Option) -> Self { Self { - file_paths, + reads_stdin: file_paths.is_none(), + // TODO: /dev/stdin doesn't work on Windows + file_paths: file_paths.unwrap_or_else(|| vec!["/dev/stdin".to_string()]), current_file: None, current_file_index: 0, remaining_bytes: length_limit, @@ -99,58 +103,71 @@ impl ChainedFileReader { offset } - fn skip_bytes(&mut self, bytes_to_skip: u64) { + /// Returns the offset the dump starts at. A skip past the end of named + /// regular files lands on their total size; stdin, devices and pipes are + /// seeked (or read) forward, so the offset is the requested skip. + fn skip_bytes(&mut self, bytes_to_skip: u64) -> u64 { let mut remaining = bytes_to_skip; + let mut skipped_whole_file = false; while remaining > 0 && self.ensure_current_file() { - match self.current_file().seek(SeekFrom::End(0)) { - Ok(file_size) => { - if remaining >= file_size { - remaining -= file_size; + let regular_size = match self.current_file().get_ref().metadata() { + Ok(metadata) if metadata.is_file() && !self.reads_stdin => Some(metadata.len()), + _ => None, + }; + + if let Some(file_size) = regular_size { + if remaining >= file_size { + remaining -= file_size; + skipped_whole_file = true; + self.current_file = None; + self.current_file_index += 1; + continue; + } + } + + let seeked = match regular_size { + Some(_) => self.current_file().seek(SeekFrom::Start(remaining)).is_ok(), + // A skip past i64::MAX can't be seeked, so it is read instead + None => i64::try_from(remaining) + .is_ok_and(|n| self.current_file().seek(SeekFrom::Current(n)).is_ok()), + }; + if seeked { + return bytes_to_skip; + } + + // This file doesn't support seeking, fall back to dummy reads + while remaining > 0 { + let mut dummy_buf = vec![0u8; remaining.min(65536) as usize]; + match self.current_file().read(&mut dummy_buf) { + Ok(0) => { self.current_file = None; self.current_file_index += 1; - } else { - match self.current_file().seek(SeekFrom::Start(remaining)) { - Ok(_) => return, - Err(e) => { - uucore::show_error!( - "cannot seek '{}': {}", - self.file_paths[self.current_file_index], - e - ); - set_exit_code(1); - self.current_file = None; - self.current_file_index += 1; - } - } + break; } - } - Err(_) => { - // This file doesn't support seeking, fall back to dummy reads - while remaining > 0 { - let mut dummy_buf = vec![0u8; remaining.min(65536) as usize]; - match self.current_file().read(&mut dummy_buf) { - Ok(0) => { - self.current_file = None; - self.current_file_index += 1; - break; - } - Ok(n) => remaining -= n as u64, - Err(e) => { - uucore::show_error!( - "cannot read '{}': {}", - self.file_paths[self.current_file_index], - e - ); - set_exit_code(1); - self.current_file = None; - self.current_file_index += 1; - break; - } - } + Ok(n) => remaining -= n as u64, + Err(e) => { + uucore::show_error!( + "cannot read '{}': {}", + self.file_paths[self.current_file_index], + e + ); + set_exit_code(1); + self.current_file = None; + self.current_file_index += 1; + break; } } } + if remaining == 0 { + return bytes_to_skip; + } + } + + if skipped_whole_file { + bytes_to_skip - remaining + } else { + bytes_to_skip } } } @@ -176,7 +193,7 @@ struct HexdumpOptions { length: Option, skip: u64, no_squeezing: bool, - files: Vec, + files: Option>, } #[uucore::main] @@ -211,20 +228,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { length: matches.get_one::("length").copied(), skip: matches.get_one::("skip").copied().unwrap_or(0), no_squeezing: matches.get_flag("no-squeezing"), - // TODO: /dev/stdin doesn't work on Windows files: matches .get_many::("files") - .map(|files| files.cloned().collect()) - .unwrap_or_else(|| vec!["/dev/stdin".to_string()]), + .map(|files| files.cloned().collect()), }; run_hexdump(options) } fn run_hexdump(options: HexdumpOptions) -> UResult<()> { let mut reader = ChainedFileReader::new(options.files.clone(), options.length); - reader.skip_bytes(options.skip); - - let mut offset = options.skip; + let mut offset = reader.skip_bytes(options.skip); let mut last_line: Vec = Vec::new(); let mut squeezing = false; diff --git a/tests/by-util/test_hexdump.rs b/tests/by-util/test_hexdump.rs index ed84ac9b..cebaa606 100644 --- a/tests/by-util/test_hexdump.rs +++ b/tests/by-util/test_hexdump.rs @@ -301,6 +301,61 @@ fn test_hexdump_invalid_skip_and_length() { .stderr_contains("invalid length"); } +#[test] +fn test_hexdump_skip_beyond_named_files() { + let scene = TestScenario::new("hexdump"); + scene.fixtures.write("file1.txt", "abc"); + scene.fixtures.write("file2.txt", "def"); + scene.fixtures.write("empty.txt", ""); + + scene + .ucmd() + .args(&["-C", "-s", "100", "file1.txt", "file2.txt"]) + .succeeds() + .stdout_is("00000006\n"); + + scene + .ucmd() + .args(&["-s", "5", "file1.txt"]) + .succeeds() + .stdout_is("0000003\n"); + + scene + .ucmd() + .args(&["-s", "5", "empty.txt"]) + .succeeds() + .stdout_is(""); +} + +#[test] +#[cfg(unix)] +fn test_hexdump_skip_beyond_named_stdin() { + let scene = TestScenario::new("hexdump"); + scene.fixtures.write("file1.txt", "abc"); + + // Named, /dev/stdin is a regular file here, so the offset is its size + scene + .ucmd() + .args(&["-s", "100", "/dev/stdin"]) + .set_stdin(std::fs::File::open(scene.fixtures.plus("file1.txt")).unwrap()) + .succeeds() + .stdout_is("0000003\n"); +} + +#[test] +#[cfg(unix)] +fn test_hexdump_skip_device() { + new_ucmd!() + .args(&["-s", "5", "/dev/null"]) + .succeeds() + .stdout_is("0000005\n"); + + new_ucmd!() + .args(&["-C", "-s", "5", "-n", "3", "/dev/zero"]) + .succeeds() + .stdout_is("00000005 00 00 00 |...|\n00000008\n"); +} + #[test] #[cfg(unix)] fn test_hexdump_skip_beyond_file() {