Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 33 additions & 16 deletions src/uu/shuf/src/shuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
},
};

let mut output = BufWriter::with_capacity(
BUF_SIZE,
match options.output {
None => Box::new(stdout()) as Box<dyn OsWrite>,
Some(ref s) => {
let file = File::create(s).map_err_context(
|| translate!("shuf-error-failed-to-open-for-writing", "file" => s.quote()),
)?;
Box::new(file) as Box<dyn OsWrite>
}
},
);

if options.head_count == 0 {
// In this case we do want to touch the output file but we can quit immediately.
// GNU still truncates the -o file in this case, but quits immediately
// without reading the input or the random source.
if let Some(ref s) = options.output {
File::create(s).map_err_context(
|| translate!("shuf-error-failed-to-open-for-writing", "file" => s.quote()),
)?;
}
return Ok(());
}

// Open the random source and read the input *before* creating the output
// file. Truncating the -o file only once the data is in hand means a failure
// here (missing input, unreadable random source) leaves an existing output
// file untouched, matching GNU and avoiding silent data loss.
let mut rng = match options.random_source {
RandomSource::None => WrappedRng::Default(rand::rng()),
RandomSource::Seed(ref seed) => WrappedRng::Seed(SeededRng::new(seed)),
Expand All @@ -153,6 +150,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
};

let fdata = match mode {
Mode::Default(ref filename) => Some(read_input_file(filename)?),
_ => None,
};

let mut output = open_output(options.output.as_deref())?;

match mode {
Mode::Echo(args) => {
let mut evec: Vec<&OsStr> = args.iter().map(AsRef::as_ref).collect();
Expand All @@ -161,8 +165,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Mode::InputRange(mut range) => {
shuf_exec(&mut range, &options, &mut rng, &mut output)?;
}
Mode::Default(filename) => {
let fdata = read_input_file(&filename)?;
Mode::Default(_) => {
let fdata = fdata.unwrap_or_default();
let mut items = split_seps(&fdata, options.sep);
shuf_exec(&mut items, &options, &mut rng, &mut output)?;
}
Expand Down Expand Up @@ -255,6 +259,19 @@ pub fn uu_app() -> Command {
)
}

fn open_output(output: Option<&Path>) -> UResult<BufWriter<Box<dyn OsWrite>>> {
let writer: Box<dyn OsWrite> = match output {
None => Box::new(stdout()),
Some(s) => {
let file = File::create(s).map_err_context(
|| translate!("shuf-error-failed-to-open-for-writing", "file" => s.quote()),
)?;
Box::new(file)
}
};
Ok(BufWriter::with_capacity(BUF_SIZE, writer))
}

fn read_input_file(filename: &Path) -> UResult<Vec<u8>> {
if filename.as_os_str() == "-" {
let mut data = Vec::new();
Expand Down
25 changes: 24 additions & 1 deletion tests/by-util/test_shuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) unwritable
// spell-checker:ignore (ToDO) unwritable GHSA
use std::fmt::Write;

use uutests::at_and_ucmd;
Expand Down Expand Up @@ -525,6 +525,29 @@ fn test_zero_head_count_file_touch_output_positive_existing() {
);
}

#[test]
fn test_output_not_truncated_when_input_missing() {
// A failure to read the input must leave an existing -o file untouched
// instead of truncating it first (data-loss regression, GHSA-5g6r-45q4-3p5r).
let (at, mut ucmd) = at_and_ucmd!();
at.write("out", "keep me\n");
ucmd.args(&["-o", "out", "does-not-exist"])
.fails_with_code(1)
.stderr_contains("does-not-exist");
assert_eq!(at.read("out"), "keep me\n");
}

#[test]
fn test_output_not_truncated_when_random_source_missing() {
// Same guarantee when the random source can't be opened.
let (at, mut ucmd) = at_and_ucmd!();
at.write("out", "keep me\n");
at.write("in", "a\nb\nc\n");
ucmd.args(&["-o", "out", "--random-source=does-not-exist", "in"])
.fails_with_code(1);
assert_eq!(at.read("out"), "keep me\n");
}

#[test]
fn test_zero_head_count_echo() {
new_ucmd!()
Expand Down
Loading