From 8b8df90e95a8837a7ecb170b53a46531852660e8 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 26 Dec 2025 04:45:05 +0000 Subject: [PATCH 1/3] csplit: detect and report write errors --- src/uu/csplit/src/csplit.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 01cc4e0dc47..0ff82d42e74 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -127,7 +127,7 @@ where let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter); // consume the rest, unless there was an error - if ret.is_ok() { + let ret = if ret.is_ok() { input_iter.rewind_buffer(); if let Some((_, line)) = input_iter.next() { // There is remaining input: create a final split and copy remainder @@ -136,14 +136,18 @@ where for (_, line) in input_iter { split_writer.writeln(&line?)?; } - split_writer.finish_split(); + split_writer.finish_split() } else if all_up_to_line && options.suppress_matched { // GNU semantics for integer patterns with --suppress-matched: // even if no remaining input, create a final (possibly empty) split split_writer.new_writer()?; - split_writer.finish_split(); + split_writer.finish_split() + } else { + Ok(()) } - } + } else { + ret + }; // delete files on error by default if ret.is_err() && !options.keep_files { split_writer.delete_all_splits()?; @@ -305,15 +309,24 @@ impl SplitWriter<'_> { /// /// # Errors /// - /// Some [`io::Error`] if the split could not be removed in case it should be elided. - fn finish_split(&mut self) { + /// Some [`CsplitError::WriteError`] if flushing the writer fails. + fn finish_split(&mut self) -> Result<(), CsplitError> { if !self.dev_null { + // Flush the writer to ensure all data is written and errors are detected + if let Some(ref mut writer) = self.current_writer { + let file_name = self.options.split_name.get(self.counter - 1); + writer + .flush() + .map_err_context(|| file_name.clone()) + .map_err(CsplitError::from)?; + } if self.options.elide_empty_files && self.size == 0 { self.counter -= 1; } else if !self.options.quiet { println!("{}", self.size); } } + Ok(()) } /// Removes all the split files that were created. @@ -379,7 +392,7 @@ impl SplitWriter<'_> { } self.writeln(&line)?; } - self.finish_split(); + self.finish_split()?; ret } @@ -446,7 +459,7 @@ impl SplitWriter<'_> { self.writeln(&line?)?; } None => { - self.finish_split(); + self.finish_split()?; return Err(CsplitError::LineOutOfRange( pattern_as_str.to_string(), )); @@ -454,7 +467,7 @@ impl SplitWriter<'_> { } offset -= 1; } - self.finish_split(); + self.finish_split()?; // if we have to suppress one line after we take the next and do nothing if next_line_suppress_matched { @@ -495,7 +508,7 @@ impl SplitWriter<'_> { ); } - self.finish_split(); + self.finish_split()?; if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); } @@ -511,7 +524,7 @@ impl SplitWriter<'_> { } } - self.finish_split(); + self.finish_split()?; Err(CsplitError::MatchNotFound(pattern_as_str.to_string())) } } From 98ecd3a799e8282e8ffbc0200fbe3636bfdd34ac Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sun, 28 Dec 2025 14:02:02 +0000 Subject: [PATCH 2/3] Add Rust integration tests for csplit write error detection --- tests/by-util/test_csplit.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index bf46063109a..76c217a29bb 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1551,3 +1551,35 @@ fn test_csplit_non_utf8_paths() { ucmd.arg(&filename).arg("3").succeeds(); } + +/// Test write error detection using /dev/full +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + // Files cleaned up by default + assert!(!at.file_exists("xx00")); +} + +/// Test write error with -k keeps files +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full_keep_files() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-k", "-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + assert!(at.file_exists("xx00")); + assert_eq!(at.read("xx00"), "1\n"); +} From 5be61ac3a49574c46006e185333f8f7dce984cd1 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sun, 28 Dec 2025 14:05:39 +0000 Subject: [PATCH 3/3] csplit: fix doc comment for finish_split --- src/uu/csplit/src/csplit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 0ff82d42e74..1c2978cea0f 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -309,7 +309,7 @@ impl SplitWriter<'_> { /// /// # Errors /// - /// Some [`CsplitError::WriteError`] if flushing the writer fails. + /// Returns an error if flushing the writer fails. fn finish_split(&mut self) -> Result<(), CsplitError> { if !self.dev_null { // Flush the writer to ensure all data is written and errors are detected