diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 08d00db18..9fb8ca7a6 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -188,9 +188,9 @@ impl<'a> Inputs<'a> { }, }; - // The index of each yielded item must be tracked for error reporting. - let mut with_idx = base.enumerate(); - let files0_from_path = settings.files0_from.as_ref().map(|i| i.as_borrowed()); + // The 1-based index of each yielded item must be tracked for error reporting. + let mut with_idx = base.enumerate().map(|(i, v)| (i + 1, v)); + let files0_from_path = settings.files0_from.as_ref().map(|p| p.as_borrowed()); let iter = iter::from_fn(move || { let (idx, next) = with_idx.next()?; @@ -303,23 +303,9 @@ fn is_stdin_small_file() -> bool { matches!(f.metadata(), Ok(meta) if meta.is_file() && meta.len() <= (10 << 20)) } -#[cfg(windows)] -fn is_stdin_small_file() -> bool { - use std::os::windows::io::{AsRawHandle, FromRawHandle}; - // Safety: we'll rely on Rust to give us a valid RawHandle for stdin with which we can attempt - // to open a File, but only for the sake of fetching .metadata(). ManuallyDrop will ensure we - // don't do anything else to the FD if anything unexpected happens. - let f = std::mem::ManuallyDrop::new(unsafe { - let h = io::stdin().as_raw_handle(); - if h.is_null() { - return false; - } - File::from_raw_handle(h) - }); - matches!(f.metadata(), Ok(meta) if meta.is_file() && meta.len() <= (10 << 20)) -} - -#[cfg(not(any(unix, windows)))] +#[cfg(not(unix))] +// Windows presents a piped stdin as a "normal file" with a length equal to however many bytes +// have been buffered at the time it's checked. To be safe, we must never assume it's a file. fn is_stdin_small_file() -> bool { false } diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 01c36b7c0..aba5ed350 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -268,12 +268,16 @@ fn test_multiple_default() { "lorem_ipsum.txt", "moby_dick.txt", "alice_in_wonderland.txt", + "alice in wonderland.txt", ]) .run() - .stdout_is( - " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ - alice_in_wonderland.txt\n 36 370 2189 total\n", - ); + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 5 57 302 alice_in_wonderland.txt\n", + " 5 57 302 alice in wonderland.txt\n", + " 41 427 2491 total\n", + )); } /// Test for an empty file. @@ -352,17 +356,24 @@ fn test_file_bytes_dictate_width() { new_ucmd!() .args(&["-lwc", "alice_in_wonderland.txt", "lorem_ipsum.txt"]) .run() - .stdout_is( - " 5 57 302 alice_in_wonderland.txt\n 13 109 772 \ - lorem_ipsum.txt\n 18 166 1074 total\n", - ); + .stdout_is(concat!( + " 5 57 302 alice_in_wonderland.txt\n", + " 13 109 772 lorem_ipsum.txt\n", + " 18 166 1074 total\n", + )); // . is a directory, so minimum_width should get set to 7 #[cfg(not(windows))] - const STDOUT: &str = " 0 0 0 emptyfile.txt\n 0 0 0 \ - .\n 0 0 0 total\n"; + const STDOUT: &str = concat!( + " 0 0 0 emptyfile.txt\n", + " 0 0 0 .\n", + " 0 0 0 total\n", + ); #[cfg(windows)] - const STDOUT: &str = " 0 0 0 emptyfile.txt\n 0 0 0 total\n"; + const STDOUT: &str = concat!( + " 0 0 0 emptyfile.txt\n", + " 0 0 0 total\n", + ); new_ucmd!() .args(&["-lwc", "emptyfile.txt", "."]) .run() @@ -392,12 +403,10 @@ fn test_read_from_directory_error() { /// Test that getting counts from nonexistent file is an error. #[test] fn test_read_from_nonexistent_file() { - const MSG: &str = "bogusfile: No such file or directory"; new_ucmd!() .args(&["bogusfile"]) .fails() - .stderr_contains(MSG) - .stdout_is(""); + .stderr_only("wc: bogusfile: No such file or directory\n"); } #[test] @@ -421,15 +430,30 @@ fn test_files0_disabled_files_argument() { #[test] fn test_files0_from() { + // file new_ucmd!() .args(&["--files0-from=files0_list.txt"]) .run() + .success() .stdout_is(concat!( " 13 109 772 lorem_ipsum.txt\n", " 18 204 1115 moby_dick.txt\n", " 5 57 302 alice_in_wonderland.txt\n", " 36 370 2189 total\n", )); + + // stream + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in_fixture("files0_list.txt") + .run() + .success() + .stdout_is(concat!( + "13 109 772 lorem_ipsum.txt\n", + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "36 370 2189 total\n", + )); } #[test] @@ -450,7 +474,7 @@ fn test_files0_from_with_stdin_in_file() { .stdout_is(concat!( " 13 109 772 lorem_ipsum.txt\n", " 18 204 1115 moby_dick.txt\n", - " 5 57 302 -\n", + " 5 57 302 -\n", // alice_in_wonderland.txt " 36 370 2189 total\n", )); } @@ -531,3 +555,152 @@ fn test_total_only() { .run() .stdout_is("31 313 1887\n"); } + +#[test] +fn test_zero_length_files() { + // A trailing zero is ignored, but otherwise empty file names are an error... + const LIST: &str = "\0moby_dick.txt\0\0alice_in_wonderland.txt\0\0lorem_ipsum.txt\0"; + + // Try with and without the last \0 + for l in [LIST.len(), LIST.len() - 1] { + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in(&LIST[..l]) + .run() + .failure() + .stdout_is(concat!( + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "13 109 772 lorem_ipsum.txt\n", + "36 370 2189 total\n", + )) + .stderr_is(concat!( + "wc: -:1: invalid zero-length file name\n", + "wc: -:3: invalid zero-length file name\n", + "wc: -:5: invalid zero-length file name\n", + )); + } + + // But, just as important, a zero-length file name may still be at the end... + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in( + LIST.as_bytes() + .iter() + .chain(b"\0") + .copied() + .collect::>(), + ) + .run() + .failure() + .stdout_is(concat!( + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "13 109 772 lorem_ipsum.txt\n", + "36 370 2189 total\n", + )) + .stderr_is(concat!( + "wc: -:1: invalid zero-length file name\n", + "wc: -:3: invalid zero-length file name\n", + "wc: -:5: invalid zero-length file name\n", + "wc: -:7: invalid zero-length file name\n", + )); +} + +#[test] +fn test_files0_errors_quoting() { + new_ucmd!() + .args(&["--files0-from=files0 with nonexistent.txt"]) + .run() + .failure() + .stderr_is(concat!( + "wc: this_file_does_not_exist.txt: No such file or directory\n", + "wc: 'files0 with nonexistent.txt':2: invalid zero-length file name\n", + "wc: 'this file does not exist.txt': No such file or directory\n", + "wc: \"this files doesn't exist either.txt\": No such file or directory\n", + )) + .stdout_is("0 0 0 total\n"); +} + +#[test] +fn test_files0_progressive_stream() { + use std::process::Stdio; + // You should be able to run wc and have a back-and-forth exchange with wc... + let mut child = new_ucmd!() + .args(&["--files0-from=-"]) + .set_stdin(Stdio::piped()) + .set_stdout(Stdio::piped()) + .set_stderr(Stdio::piped()) + .run_no_wait(); + + macro_rules! chk { + ($fn:ident, $exp:literal) => { + assert_eq!(child.$fn($exp.len()), $exp.as_bytes()); + }; + } + + // File in, count out... + child.write_in("moby_dick.txt\0"); + chk!(stdout_exact_bytes, "18 204 1115 moby_dick.txt\n"); + child.write_in("lorem_ipsum.txt\0"); + chk!(stdout_exact_bytes, "13 109 772 lorem_ipsum.txt\n"); + + // Introduce an error! + child.write_in("\0"); + chk!( + stderr_exact_bytes, + "wc: -:3: invalid zero-length file name\n" + ); + + // wc is quick to forgive, let's move on... + child.write_in("alice_in_wonderland.txt\0"); + chk!(stdout_exact_bytes, "5 57 302 alice_in_wonderland.txt\n"); + + // Fin. + child + .wait() + .expect("wc should finish") + .failure() + .stdout_only("36 370 2189 total\n"); +} + +#[test] +fn files0_from_dir() { + // On Unix, `read(open("."))` fails. On Windows, `open(".")` fails. Thus, the errors happen in + // different contexts. + #[cfg(not(windows))] + macro_rules! dir_err { + ($p:literal) => { + concat!("wc: ", $p, ": read error: Is a directory\n") + }; + } + #[cfg(windows)] + macro_rules! dir_err { + ($p:literal) => { + concat!("wc: cannot open ", $p, " for reading: Permission denied\n") + }; + } + + new_ucmd!() + .args(&["--files0-from=dir with spaces"]) + .fails() + .stderr_only(dir_err!("'dir with spaces'")); + + // Those contexts have different rules about quoting in errors... + #[cfg(windows)] + const DOT_ERR: &str = dir_err!("'.'"); + #[cfg(not(windows))] + const DOT_ERR: &str = dir_err!("."); + new_ucmd!() + .args(&["--files0-from=."]) + .fails() + .stderr_only(DOT_ERR); + + // That also means you cannot `< . wc --files0-from=-` on Windows. + #[cfg(not(windows))] + new_ucmd!() + .args(&["--files0-from=-"]) + .set_stdin(std::fs::File::open(".").unwrap()) + .fails() + .stderr_only(dir_err!("-")); +} diff --git a/tests/fixtures/wc/alice in wonderland.txt b/tests/fixtures/wc/alice in wonderland.txt new file mode 100644 index 000000000..a95562a1c --- /dev/null +++ b/tests/fixtures/wc/alice in wonderland.txt @@ -0,0 +1,5 @@ +Alice was beginning to get very tired of sitting by +her sister on the bank, and of having nothing to do: once or twice +she had peeped into the book her sister was reading, but it had no +pictures or conversations in it, "and what is the use of a book," +thought Alice "without pictures or conversation?" diff --git a/tests/fixtures/wc/dir with spaces/.keep b/tests/fixtures/wc/dir with spaces/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/wc/files0 with nonexistent.txt b/tests/fixtures/wc/files0 with nonexistent.txt new file mode 100644 index 000000000..00c00b705 Binary files /dev/null and b/tests/fixtures/wc/files0 with nonexistent.txt differ