mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
7d27b3baa1
Follow-up to the metadata TOCTOU fix (#11972, PR #12439): - Add a syscall-level regression guard in util/check-safe-traversal.sh: under strace, head must read metadata from the open descriptor (fstat/statx on the fd) and must not stat the path before opening it. Verified this fails on the pre-fix code and passes after. - Add an integration test asserting that an unreadable file produces an error but no "==> name <==" header, matching GNU (the header is only printed after a successful open). - Fix a stray paren in the Windows-branch comment.
1008 lines
28 KiB
Rust
1008 lines
28 KiB
Rust
// This file is part of the uutils coreutils package.
|
|
//
|
|
// For the full copyright and license information, please view the LICENSE
|
|
// file that was distributed with this source code.
|
|
|
|
// spell-checker:ignore (words) bogusfile emptyfile abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
use std::io::Read;
|
|
use uutests::new_ucmd;
|
|
use uutests::util::TestScenario;
|
|
use uutests::util_name;
|
|
static INPUT: &str = "lorem_ipsum.txt";
|
|
|
|
#[test]
|
|
fn test_invalid_arg() {
|
|
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_default() {
|
|
new_ucmd!()
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_1_line_obsolete() {
|
|
new_ucmd!()
|
|
.args(&["-1"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_1_line() {
|
|
new_ucmd!()
|
|
.args(&["-n", "1"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_negative_23_line() {
|
|
new_ucmd!()
|
|
.args(&["-n", "-23"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_5_chars() {
|
|
new_ucmd!()
|
|
.args(&["-c", "5"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_5_chars.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_default() {
|
|
new_ucmd!()
|
|
.arg(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_1_line_obsolete() {
|
|
new_ucmd!()
|
|
.args(&["-1", INPUT])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_1_line() {
|
|
new_ucmd!()
|
|
.args(&["-n", "1", INPUT])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_1_line_presume_input_pipe() {
|
|
new_ucmd!()
|
|
.args(&["---presume-input-pipe", "-n", "1", INPUT])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_1_line.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_5_chars() {
|
|
new_ucmd!()
|
|
.args(&["-c", "5", INPUT])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_5_chars.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_verbose() {
|
|
new_ucmd!()
|
|
.args(&["-v", INPUT])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_verbose.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_spams_newline() {
|
|
new_ucmd!().pipe_in("a").succeeds().stdout_is("a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_byte_syntax() {
|
|
new_ucmd!()
|
|
.args(&["-1c"])
|
|
.pipe_in("abc")
|
|
.succeeds()
|
|
.stdout_is("a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_line_syntax() {
|
|
new_ucmd!()
|
|
.args(&["-n", "2048m"])
|
|
.pipe_in("a\n")
|
|
.succeeds()
|
|
.stdout_is("a\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_terminated_syntax() {
|
|
new_ucmd!()
|
|
.args(&["-z", "-n", "1"])
|
|
.pipe_in("x\0y")
|
|
.succeeds()
|
|
.stdout_is("x\0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_terminated_syntax_2() {
|
|
new_ucmd!()
|
|
.args(&["-z", "-n", "2"])
|
|
.pipe_in("x\0y")
|
|
.succeeds()
|
|
.stdout_is("x\0y");
|
|
}
|
|
|
|
#[test]
|
|
fn test_non_terminated_input() {
|
|
new_ucmd!()
|
|
.args(&["-n", "-1"])
|
|
.pipe_in("x\ny")
|
|
.succeeds()
|
|
.stdout_is("x\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_terminated_negative_lines() {
|
|
new_ucmd!()
|
|
.args(&["-z", "-n", "-1"])
|
|
.pipe_in("x\0y\0z\0")
|
|
.succeeds()
|
|
.stdout_is("x\0y\0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_byte_syntax() {
|
|
new_ucmd!()
|
|
.args(&["--bytes=-2"])
|
|
.pipe_in("a\n")
|
|
.succeeds()
|
|
.stdout_is("");
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_bytes_greater_than_input_size_stdin() {
|
|
new_ucmd!()
|
|
.args(&["-c", "-2"])
|
|
.pipe_in("a")
|
|
.succeeds()
|
|
.no_output();
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_bytes_greater_than_input_size_file() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.write_bytes("f", b"a");
|
|
ts.ucmd().args(&["-c", "-2", "f"]).succeeds().no_output();
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_zero_lines() {
|
|
new_ucmd!()
|
|
.arg("--lines=-0")
|
|
.pipe_in("a\nb\n")
|
|
.succeeds()
|
|
.stdout_is("a\nb\n");
|
|
new_ucmd!()
|
|
.arg("--lines=-0")
|
|
.pipe_in("a\nb")
|
|
.succeeds()
|
|
.stdout_is("a\nb");
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_zero_bytes() {
|
|
new_ucmd!()
|
|
.args(&["--bytes=-0"])
|
|
.pipe_in("qwerty")
|
|
.succeeds()
|
|
.stdout_is("qwerty");
|
|
}
|
|
#[test]
|
|
fn test_no_such_file_or_directory() {
|
|
new_ucmd!()
|
|
.arg("no_such_file.toml")
|
|
.fails()
|
|
.stderr_contains("cannot open 'no_such_file.toml' for reading: No such file or directory");
|
|
}
|
|
|
|
#[test]
|
|
fn test_lines_leading_zeros() {
|
|
new_ucmd!()
|
|
.arg("--lines=010")
|
|
.pipe_in("\n\n\n\n\n\n\n\n\n\n\n\n")
|
|
.succeeds()
|
|
.stdout_is("\n\n\n\n\n\n\n\n\n\n");
|
|
}
|
|
|
|
/// Test that each non-existent files gets its own error message printed.
|
|
#[test]
|
|
fn test_multiple_nonexistent_files() {
|
|
new_ucmd!()
|
|
.args(&["bogusfile1", "bogusfile2"])
|
|
.fails()
|
|
.stdout_does_not_contain("==> bogusfile1 <==")
|
|
.stderr_contains("cannot open 'bogusfile1' for reading: No such file or directory")
|
|
.stdout_does_not_contain("==> bogusfile2 <==")
|
|
.stderr_contains("cannot open 'bogusfile2' for reading: No such file or directory");
|
|
}
|
|
|
|
// there was a bug not caught by previous tests
|
|
// where for negative n > 3, the total amount of lines
|
|
// was correct, but it would eat from the second line
|
|
#[test]
|
|
fn test_sequence_fixture() {
|
|
new_ucmd!()
|
|
.args(&["-n", "-10", "sequence"])
|
|
.succeeds()
|
|
.stdout_is_fixture("sequence.expected");
|
|
}
|
|
#[test]
|
|
fn test_file_backwards() {
|
|
new_ucmd!()
|
|
.args(&["-c", "-10", "lorem_ipsum.txt"])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_backwards_file.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_terminated() {
|
|
new_ucmd!()
|
|
.args(&["-z", "zero_terminated.txt"])
|
|
.succeeds()
|
|
.stdout_is_fixture("zero_terminated.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_obsolete_extras() {
|
|
new_ucmd!()
|
|
.args(&["-5zv"])
|
|
.pipe_in("1\x002\x003\x004\x005\x006")
|
|
.succeeds()
|
|
.stdout_is("==> standard input <==\n1\x002\x003\x004\x005\0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_files() {
|
|
new_ucmd!()
|
|
.args(&["emptyfile.txt", "emptyfile.txt"])
|
|
.succeeds()
|
|
.stdout_is("==> emptyfile.txt <==\n\n==> emptyfile.txt <==\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_files_with_stdin() {
|
|
new_ucmd!()
|
|
.args(&["emptyfile.txt", "-", "emptyfile.txt"])
|
|
.pipe_in("hello\n")
|
|
.succeeds()
|
|
.stdout_is(
|
|
"==> emptyfile.txt <==
|
|
|
|
==> standard input <==
|
|
hello
|
|
|
|
==> emptyfile.txt <==
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bad_utf8() {
|
|
let bytes: &[u8] = b"\xfc\x80\x80\x80\x80\xaf";
|
|
new_ucmd!()
|
|
.args(&["-c", "6"])
|
|
.pipe_in(bytes)
|
|
.succeeds()
|
|
.stdout_is_bytes(bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bad_utf8_lines() {
|
|
let input: &[u8] =
|
|
b"\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf";
|
|
let output = b"\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf\n";
|
|
new_ucmd!()
|
|
.args(&["-n", "2"])
|
|
.pipe_in(input)
|
|
.succeeds()
|
|
.stdout_is_bytes(output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_head_invalid_num() {
|
|
new_ucmd!()
|
|
.args(&["-c", "1024R", "emptyfile.txt"])
|
|
.succeeds()
|
|
.no_output();
|
|
new_ucmd!()
|
|
.args(&["-n", "1024R", "emptyfile.txt"])
|
|
.succeeds()
|
|
.no_output();
|
|
new_ucmd!()
|
|
.args(&["-c", "1Y", "emptyfile.txt"])
|
|
.succeeds()
|
|
.no_output();
|
|
new_ucmd!()
|
|
.args(&["-n", "1Y", "emptyfile.txt"])
|
|
.succeeds()
|
|
.no_output();
|
|
#[cfg(target_pointer_width = "32")]
|
|
{
|
|
let sizes = ["1000G", "10T"];
|
|
for size in &sizes {
|
|
new_ucmd!().args(&["-c", size]).succeeds();
|
|
}
|
|
}
|
|
#[cfg(target_pointer_width = "32")]
|
|
{
|
|
let sizes = ["-1000G", "-10T"];
|
|
for size in &sizes {
|
|
new_ucmd!().args(&["-c", size]).succeeds().no_output();
|
|
}
|
|
}
|
|
new_ucmd!()
|
|
.args(&["-c", "-³"])
|
|
.fails()
|
|
.stderr_is("head: invalid number of bytes: '³'\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_head_num_with_undocumented_sign_bytes() {
|
|
// tail: '-' is not documented (8.32 man pages)
|
|
// head: '+' is not documented (8.32 man pages)
|
|
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
|
|
new_ucmd!()
|
|
.args(&["-c", "5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("abcde");
|
|
new_ucmd!()
|
|
.args(&["-c", "-5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("abcdefghijklmnopqrstu");
|
|
new_ucmd!()
|
|
.args(&["-c", "+5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("abcde");
|
|
}
|
|
|
|
#[test]
|
|
fn test_presume_input_pipe_default() {
|
|
new_ucmd!()
|
|
.args(&["---presume-input-pipe"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_presume_input_pipe_5_chars() {
|
|
new_ucmd!()
|
|
.args(&["-c", "5", "---presume-input-pipe"])
|
|
.pipe_in_fixture(INPUT)
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_5_chars.expected");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI: no subprocess spawning")]
|
|
fn test_all_but_last_bytes_large_file_piped() {
|
|
// Validate print-all-but-last-n-bytes with a large piped-in (i.e. non-seekable) file.
|
|
let scene = TestScenario::new(util_name!());
|
|
let fixtures = &scene.fixtures;
|
|
|
|
// First, create all our fixtures.
|
|
let seq_20000_file_name = "seq_20000";
|
|
let seq_19000_file_name = "seq_19000";
|
|
let seq_19001_20000_file_name = "seq_19001_20000";
|
|
scene
|
|
.cmd("seq")
|
|
.arg("20000")
|
|
.set_stdout(fixtures.make_file(seq_20000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.arg("19000")
|
|
.set_stdout(fixtures.make_file(seq_19000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.args(&["19001", "20000"])
|
|
.set_stdout(fixtures.make_file(seq_19001_20000_file_name))
|
|
.succeeds();
|
|
|
|
let seq_19001_20000_file_length = fixtures
|
|
.open(seq_19001_20000_file_name)
|
|
.metadata()
|
|
.unwrap()
|
|
.len();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", &format!("-{seq_19001_20000_file_length}")])
|
|
.pipe_in_fixture(seq_20000_file_name)
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_19000_file_name);
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_but_last_lines_large_file_presume_input_pipe() {
|
|
// Validate print-all-but-last-n-lines on the non-seekable path for a large terminated input.
|
|
// This input shape forces repeated buffer reuse while some chunks end mid-line.
|
|
let scene = TestScenario::new(util_name!());
|
|
let fixtures = &scene.fixtures;
|
|
|
|
let input_file_name = "reused_line_chunks";
|
|
let expected_output_file_name = "reused_line_chunks_elide_last";
|
|
let line = "aaaaaa\n";
|
|
let input_line_count: usize = 20_000;
|
|
|
|
let mut input = String::with_capacity(line.len() * input_line_count);
|
|
for _ in 0..input_line_count {
|
|
input.push_str(line);
|
|
}
|
|
fixtures.write(input_file_name, &input);
|
|
fixtures.write(
|
|
expected_output_file_name,
|
|
&line.repeat(input_line_count - 1),
|
|
);
|
|
|
|
scene
|
|
.ucmd()
|
|
.args(&["---presume-input-pipe", "-n", "-1", input_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture(expected_output_file_name);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI: no subprocess spawning")]
|
|
fn test_all_but_last_lines_large_file() {
|
|
// Create our fixtures on the fly. We need the input file to be at least double
|
|
// the size of BUF_SIZE as specified in head.rs. Go for something a bit bigger
|
|
// than that.
|
|
let scene = TestScenario::new(util_name!());
|
|
let fixtures = &scene.fixtures;
|
|
let seq_20000_file_name = "seq_20000";
|
|
let seq_20000_truncated_file_name = "seq_20000_truncated";
|
|
let seq_1000_file_name = "seq_1000";
|
|
scene
|
|
.cmd("seq")
|
|
.arg("20000")
|
|
.set_stdout(fixtures.make_file(seq_20000_file_name))
|
|
.succeeds();
|
|
// Create a file the same as seq_20000 except for the final terminating endline.
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", "-1", seq_20000_file_name])
|
|
.set_stdout(fixtures.make_file(seq_20000_truncated_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.arg("1000")
|
|
.set_stdout(fixtures.make_file(seq_1000_file_name))
|
|
.succeeds();
|
|
|
|
// Now run our tests.
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-19000", seq_20000_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_1000_file_name);
|
|
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-20000", seq_20000_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture("emptyfile.txt");
|
|
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-20001", seq_20000_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture("emptyfile.txt");
|
|
|
|
// Confirm correct behavior when the input file doesn't end with a newline.
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-19000", seq_20000_truncated_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_1000_file_name);
|
|
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-20000", seq_20000_truncated_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture("emptyfile.txt");
|
|
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-20001", seq_20000_truncated_file_name])
|
|
.succeeds()
|
|
.stdout_only_fixture("emptyfile.txt");
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(
|
|
wasi_runner,
|
|
ignore = "WASI: stdin file position not preserved through wasmtime"
|
|
)]
|
|
fn test_validate_stdin_offset_lines() {
|
|
// A handful of unix-only tests to validate behavior when reading from stdin on a seekable
|
|
// file. GNU-compatibility requires that the stdin file be left such that if another
|
|
// process is invoked on the same stdin file after head has run, the subsequent file should
|
|
// start reading from the byte after the last byte printed by head.
|
|
// Since this is unix-only requirement, keep this as a separate test rather than adding a
|
|
// conditionally-compiled segment to multiple tests.
|
|
//
|
|
// Test scenarios...
|
|
// 1 - Print the first n lines
|
|
// 2 - Print all-but the last n lines
|
|
// 3 - Print all but the last n lines, large file.
|
|
let scene = TestScenario::new(util_name!());
|
|
let fixtures = &scene.fixtures;
|
|
|
|
// Test 1 - Print the first n lines
|
|
fixtures.write("f1", "a\nb\nc\n");
|
|
let file = fixtures.open("f1");
|
|
let mut file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "1"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only("a\n");
|
|
let mut bytes_remaining_in_stdin = vec![];
|
|
assert_eq!(
|
|
file_shadow
|
|
.read_to_end(&mut bytes_remaining_in_stdin)
|
|
.unwrap(),
|
|
4
|
|
);
|
|
assert_eq!(
|
|
String::from_utf8(bytes_remaining_in_stdin).unwrap(),
|
|
"b\nc\n"
|
|
);
|
|
|
|
// Test 2 - Print all-but the last n lines
|
|
fixtures.write("f2", "a\nb\nc\n");
|
|
let file = fixtures.open("f2");
|
|
let mut file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-1"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only("a\nb\n");
|
|
let mut bytes_remaining_in_stdin = vec![];
|
|
assert_eq!(
|
|
file_shadow
|
|
.read_to_end(&mut bytes_remaining_in_stdin)
|
|
.unwrap(),
|
|
2
|
|
);
|
|
assert_eq!(String::from_utf8(bytes_remaining_in_stdin).unwrap(), "c\n");
|
|
|
|
// Test 3 - Print all but the last n lines, large input file.
|
|
// First, create all our fixtures.
|
|
let seq_20000_file_name = "seq_20000";
|
|
let seq_1000_file_name = "seq_1000";
|
|
let seq_1001_20000_file_name = "seq_1001_20000";
|
|
scene
|
|
.cmd("seq")
|
|
.arg("20000")
|
|
.set_stdout(fixtures.make_file(seq_20000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.arg("1000")
|
|
.set_stdout(fixtures.make_file(seq_1000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.args(&["1001", "20000"])
|
|
.set_stdout(fixtures.make_file(seq_1001_20000_file_name))
|
|
.succeeds();
|
|
|
|
let file = fixtures.open(seq_20000_file_name);
|
|
let file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-n", "-19000"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_1000_file_name);
|
|
scene
|
|
.cmd("cat")
|
|
.set_stdin(file_shadow)
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_1001_20000_file_name);
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(
|
|
wasi_runner,
|
|
ignore = "WASI: stdin file position not preserved through wasmtime"
|
|
)]
|
|
fn test_validate_stdin_offset_bytes() {
|
|
// A handful of unix-only tests to validate behavior when reading from stdin on a seekable
|
|
// file. GNU-compatibility requires that the stdin file be left such that if another
|
|
// process is invoked on the same stdin file after head has run, the subsequent file should
|
|
// start reading from the byte after the last byte printed by head.
|
|
// Since this is unix-only requirement, keep this as a separate test rather than adding a
|
|
// conditionally-compiled segment to multiple tests.
|
|
//
|
|
// Test scenarios...
|
|
// 1 - Print the first n bytes
|
|
// 2 - Print all-but the last n bytes
|
|
// 3 - Print all-but the last n bytes, with n=0 (i.e. print everything)
|
|
// 4 - Print all but the last n bytes, large file.
|
|
let scene = TestScenario::new(util_name!());
|
|
let fixtures = &scene.fixtures;
|
|
|
|
// Test 1 - Print the first n bytes
|
|
fixtures.write("f1", "abc\ndef\n");
|
|
let file = fixtures.open("f1");
|
|
let mut file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", "2"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only("ab");
|
|
let mut bytes_remaining_in_stdin = vec![];
|
|
assert_eq!(
|
|
file_shadow
|
|
.read_to_end(&mut bytes_remaining_in_stdin)
|
|
.unwrap(),
|
|
6
|
|
);
|
|
assert_eq!(
|
|
String::from_utf8(bytes_remaining_in_stdin).unwrap(),
|
|
"c\ndef\n"
|
|
);
|
|
|
|
// Test 2 - Print all-but the last n bytes
|
|
fixtures.write("f2", "abc\ndef\n");
|
|
let file = fixtures.open("f2");
|
|
let mut file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", "-3"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only("abc\nd");
|
|
let mut bytes_remaining_in_stdin = vec![];
|
|
assert_eq!(
|
|
file_shadow
|
|
.read_to_end(&mut bytes_remaining_in_stdin)
|
|
.unwrap(),
|
|
3
|
|
);
|
|
assert_eq!(String::from_utf8(bytes_remaining_in_stdin).unwrap(), "ef\n");
|
|
|
|
// Test 3 - Print all-but the last n bytes, n=0 (i.e. print everything)
|
|
fixtures.write("f3", "abc\ndef\n");
|
|
let file = fixtures.open("f3");
|
|
let mut file_shadow = file.try_clone().unwrap();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", "-0"])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only("abc\ndef\n");
|
|
let mut bytes_remaining_in_stdin = vec![];
|
|
assert_eq!(
|
|
file_shadow
|
|
.read_to_end(&mut bytes_remaining_in_stdin)
|
|
.unwrap(),
|
|
0
|
|
);
|
|
assert_eq!(String::from_utf8(bytes_remaining_in_stdin).unwrap(), "");
|
|
|
|
// Test 4 - Print all but the last n bytes, large input file.
|
|
// First, create all our fixtures.
|
|
let seq_20000_file_name = "seq_20000";
|
|
let seq_19000_file_name = "seq_19000";
|
|
let seq_19001_20000_file_name = "seq_19001_20000";
|
|
scene
|
|
.cmd("seq")
|
|
.arg("20000")
|
|
.set_stdout(fixtures.make_file(seq_20000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.arg("19000")
|
|
.set_stdout(fixtures.make_file(seq_19000_file_name))
|
|
.succeeds();
|
|
scene
|
|
.cmd("seq")
|
|
.args(&["19001", "20000"])
|
|
.set_stdout(fixtures.make_file(seq_19001_20000_file_name))
|
|
.succeeds();
|
|
|
|
let file = fixtures.open(seq_20000_file_name);
|
|
let file_shadow = file.try_clone().unwrap();
|
|
let seq_19001_20000_file_length = fixtures
|
|
.open(seq_19001_20000_file_name)
|
|
.metadata()
|
|
.unwrap()
|
|
.len();
|
|
scene
|
|
.ucmd()
|
|
.args(&["-c", &format!("-{seq_19001_20000_file_length}")])
|
|
.set_stdin(file)
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_19000_file_name);
|
|
scene
|
|
.cmd("cat")
|
|
.set_stdin(file_shadow)
|
|
.succeeds()
|
|
.stdout_only_fixture(seq_19001_20000_file_name);
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/proc) not visible")]
|
|
fn test_read_backwards_bytes_proc_fs_version() {
|
|
let ts = TestScenario::new(util_name!());
|
|
|
|
let args = ["-c", "-1", "/proc/version"];
|
|
let result = ts.ucmd().args(&args).succeeds();
|
|
assert!(!result.stdout().is_empty());
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/proc) not visible")]
|
|
fn test_read_backwards_bytes_proc_fs_modules() {
|
|
let ts = TestScenario::new(util_name!());
|
|
|
|
let args = ["-c", "-1", "/proc/modules"];
|
|
let result = ts.ucmd().args(&args).succeeds();
|
|
|
|
// Only expect output if the file is not empty, e.g. it is empty in default WSL2.
|
|
if !ts.fixtures.read("/proc/modules").is_empty() {
|
|
assert!(!result.stdout().is_empty());
|
|
}
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/proc) not visible")]
|
|
fn test_read_backwards_lines_proc_fs_modules() {
|
|
let ts = TestScenario::new(util_name!());
|
|
|
|
let args = ["--lines", "-1", "/proc/modules"];
|
|
let result = ts.ucmd().args(&args).succeeds();
|
|
|
|
// Only expect output if the file is not empty, e.g. it is empty in default WSL2.
|
|
if !ts.fixtures.read("/proc/modules").is_empty() {
|
|
assert!(!result.stdout().is_empty());
|
|
}
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "windows"),
|
|
not(target_os = "macos"),
|
|
not(target_os = "android"),
|
|
not(target_os = "freebsd"),
|
|
not(target_os = "openbsd")
|
|
))]
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/sys) not visible")]
|
|
fn test_read_backwards_bytes_sys_kernel_profiling() {
|
|
let ts = TestScenario::new(util_name!());
|
|
// in case the kernel was not built with profiling support, e.g. WSL
|
|
if !ts.fixtures.file_exists("/sys/kernel/profiling") {
|
|
println!("test skipped: /sys/kernel/profiling does not exist");
|
|
return;
|
|
}
|
|
let args = ["-c", "-1", "/sys/kernel/profiling"];
|
|
let result = ts.ucmd().args(&args).succeeds();
|
|
let stdout_str = result.stdout_str();
|
|
assert_eq!(stdout_str.len(), 1);
|
|
assert!(stdout_str == "0" || stdout_str == "1");
|
|
}
|
|
|
|
#[test]
|
|
fn test_value_too_large() {
|
|
const MAX: u64 = u64::MAX;
|
|
|
|
new_ucmd!()
|
|
.args(&["-n", format!("{MAX}0").as_str(), "lorem_ipsum.txt"])
|
|
.succeeds();
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_but_last_lines() {
|
|
new_ucmd!()
|
|
.args(&["-n", "-15", "lorem_ipsum.txt"])
|
|
.succeeds()
|
|
.stdout_is_fixture("lorem_ipsum_backwards_15_lines.expected");
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
|
|
#[test]
|
|
fn test_write_to_dev_full() {
|
|
use std::fs::OpenOptions;
|
|
|
|
for append in [true, false] {
|
|
{
|
|
let dev_full = OpenOptions::new()
|
|
.write(true)
|
|
.append(append)
|
|
.open("/dev/full")
|
|
.unwrap();
|
|
|
|
new_ucmd!()
|
|
.pipe_in_fixture(INPUT)
|
|
.set_stdout(dev_full)
|
|
.fails()
|
|
.stderr_is("head: error writing 'standard output': No space left on device\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")]
|
|
fn test_head_non_utf8_paths() {
|
|
use std::ffi::OsStr;
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
let scene = TestScenario::new(util_name!());
|
|
let at = &scene.fixtures;
|
|
|
|
// Create a test file with non-UTF-8 bytes in the name
|
|
let non_utf8_bytes = b"test_\xFF\xFE.txt";
|
|
let non_utf8_name = OsStr::from_bytes(non_utf8_bytes);
|
|
|
|
std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap();
|
|
|
|
let result = scene.ucmd().arg(non_utf8_name).succeeds();
|
|
|
|
let output = result.stdout_str_lossy();
|
|
assert!(output.contains("line1"));
|
|
assert!(output.contains("line2"));
|
|
assert!(output.contains("line3"));
|
|
}
|
|
// Test that head handles non-UTF-8 file names without crashing
|
|
|
|
#[test]
|
|
fn test_do_not_attempt_to_read_a_directory() {
|
|
new_ucmd!()
|
|
.arg(".")
|
|
.fails_with_code(1)
|
|
.stderr_contains("error reading '.'");
|
|
}
|
|
|
|
/// Regression test for https://github.com/uutils/coreutils/issues/12215
|
|
/// `head -c0 <directory>` should succeed (nothing to read), matching GNU.
|
|
#[test]
|
|
fn test_zero_bytes_on_directory_succeeds() {
|
|
new_ucmd!().args(&["-c", "0", "."]).succeeds().no_output();
|
|
}
|
|
|
|
/// `head -n0 <directory>` should also succeed.
|
|
#[test]
|
|
fn test_zero_lines_on_directory_succeeds() {
|
|
new_ucmd!().args(&["-n", "0", "."]).succeeds().no_output();
|
|
}
|
|
|
|
/// GNU `head` prints the `==> name <==` header for every file argument
|
|
/// (including directories) when invoked with multiple files. With non-zero
|
|
/// output, directories also produce an "Is a directory" error after the
|
|
/// header.
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn test_directory_header_with_multiple_files() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.mkdir("d");
|
|
at.write("f", "hello\n");
|
|
ts.ucmd()
|
|
.args(&["-c", "5", "d", "f"])
|
|
.fails_with_code(1)
|
|
.stdout_is("==> d <==\n\n==> f <==\nhello")
|
|
.stderr_contains("Is a directory");
|
|
}
|
|
|
|
/// With `-c 0` (zero output), the directory header is still printed but no
|
|
/// error is emitted.
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn test_directory_header_with_multiple_files_zero_output() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.mkdir("d");
|
|
at.write("f", "hello\n");
|
|
ts.ucmd()
|
|
.args(&["-c", "0", "d", "f"])
|
|
.succeeds()
|
|
.stdout_is("==> d <==\n\n==> f <==\n")
|
|
.no_stderr();
|
|
}
|
|
|
|
/// GNU `head` prints the `==> name <==` header only after the file is
|
|
/// successfully opened. A file that exists but cannot be opened (e.g. no read
|
|
/// permission) must therefore produce only an error and no header.
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn test_unreadable_file_prints_no_header() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.write("unreadable", "secret\n");
|
|
at.write("readable", "hello\n");
|
|
at.set_mode("unreadable", 0o000);
|
|
|
|
// Running as root bypasses permission checks, so the open would succeed.
|
|
if std::fs::File::open(at.plus("unreadable")).is_ok() {
|
|
return;
|
|
}
|
|
|
|
ts.ucmd()
|
|
.args(&["-c", "5", "unreadable", "readable"])
|
|
.fails_with_code(1)
|
|
.stdout_is("==> readable <==\nhello")
|
|
.stdout_does_not_contain("==> unreadable <==")
|
|
.stderr_contains("cannot open 'unreadable' for reading: Permission denied");
|
|
}
|