Skip to content

Commit a540ab3

Browse files
committed
split: fix I/O error handling for device full conditions
Should fix tests/split/split-io-err.sh
1 parent 995c9e0 commit a540ab3

5 files changed

Lines changed: 172 additions & 14 deletions

File tree

src/uu/split/locales/en-US.ftl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ split-error-cannot-determine-input-size = { $input }: cannot determine input siz
3838
split-error-cannot-determine-file-size = { $input }: cannot determine file size
3939
split-error-cannot-read-from-input = { $input }: cannot read from input : { $error }
4040
split-error-input-output-error = input/output error
41+
split-error-flush-before-file-switch = flush error before file switch
42+
split-error-flush-already-reported = flush error already reported
43+
split-error-write-already-reported = write error already reported
44+
split-error-flush-in-chunk-writer = flush error in ByteChunkWriter
4145
split-error-unable-to-open-file = unable to open { $file }; aborting
4246
split-error-unable-to-reopen-file = unable to re-open { $file }; aborting
4347
split-error-file-descriptor-limit = at file descriptor limit, but no file descriptor left to close. Closed { $count } writers before.

src/uu/split/locales/fr-FR.ftl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,16 @@ split-error-cannot-determine-input-size = { $input } : impossible de déterminer
3838
split-error-cannot-determine-file-size = { $input } : impossible de déterminer la taille du fichier
3939
split-error-cannot-read-from-input = { $input } : impossible de lire depuis l'entrée : { $error }
4040
split-error-input-output-error = erreur d'entrée/sortie
41+
split-error-flush-before-file-switch = erreur de vidage avant changement de fichier
42+
split-error-flush-already-reported = erreur de vidage déjà signalée
43+
split-error-write-already-reported = erreur d'écriture déjà signalée
44+
split-error-flush-in-chunk-writer = erreur de vidage dans ByteChunkWriter
4145
split-error-unable-to-open-file = impossible d'ouvrir { $file } ; abandon
4246
split-error-unable-to-reopen-file = impossible de rouvrir { $file } ; abandon
4347
split-error-file-descriptor-limit = limite de descripteurs de fichiers atteinte, mais aucun descripteur de fichier à fermer. { $count } écrivains fermés auparavant.
4448
split-error-shell-process-returned = Le processus shell a retourné { $code }
4549
split-error-shell-process-terminated = Le processus shell a été terminé par un signal
50+
split-error-is-a-directory = { $dir } : Est un répertoire
4651
4752
# Messages d'aide pour les options de ligne de commande
4853
split-help-bytes = mettre TAILLE octets par fichier de sortie

src/uu/split/src/platform/unix.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,27 @@ pub fn instantiate_current_writer(
143143
ErrorKind::IsADirectory => Error::other(
144144
translate!("split-error-is-a-directory", "dir" => filename),
145145
),
146-
_ => Error::other(
146+
ErrorKind::PermissionDenied => Error::other(
147147
translate!("split-error-unable-to-open-file", "file" => filename),
148148
),
149+
_ => Error::other(format!(
150+
"split: {filename}: {}",
151+
uucore::error::strip_errno(&e)
152+
)),
149153
})?
150154
} else {
151155
// re-open file that we previously created to append to it
152156
std::fs::OpenOptions::new()
153157
.append(true)
154158
.open(Path::new(&filename))
155-
.map_err(|_| {
156-
Error::other(
159+
.map_err(|e| match e.kind() {
160+
ErrorKind::PermissionDenied => Error::other(
157161
translate!("split-error-unable-to-reopen-file", "file" => filename),
158-
)
162+
),
163+
_ => Error::other(format!(
164+
"split: {filename}: {}",
165+
uucore::error::strip_errno(&e)
166+
)),
159167
})?
160168
};
161169
Ok(BufWriter::new(Box::new(file) as Box<dyn Write>))

src/uu/split/src/split.rs

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Wr
2121
use std::path::Path;
2222
use thiserror::Error;
2323
use uucore::display::Quotable;
24-
use uucore::error::{FromIo, UIoError, UResult, USimpleError, UUsageError};
24+
use uucore::error::{
25+
FromIo, UIoError, UResult, USimpleError, UUsageError, set_exit_code, strip_errno,
26+
};
2527
use uucore::translate;
2628

2729
use uucore::parser::parse_size::parse_size_u64;
@@ -565,11 +567,48 @@ fn ignorable_io_error(error: &io::Error, settings: &Settings) -> bool {
565567
/// If ignorable io error occurs, return number of bytes as if all bytes written
566568
/// Should not be used for Kth chunk number sub-strategies
567569
/// as those do not work with `--filter` option
568-
fn custom_write<T: Write>(bytes: &[u8], writer: &mut T, settings: &Settings) -> io::Result<usize> {
570+
///
571+
/// When `error_context` is None, acts as simple write wrapper
572+
/// When `error_context` is Some((filename, error_reported)), enables error reporting and immediate flushing
573+
fn custom_write<T: Write>(
574+
bytes: &[u8],
575+
writer: &mut T,
576+
settings: &Settings,
577+
error_context: Option<(&str, &mut bool)>,
578+
) -> io::Result<usize> {
569579
match writer.write(bytes) {
570-
Ok(n) => Ok(n),
580+
Ok(n) => {
581+
// If error reporting is enabled, flush immediately to catch buffered I/O errors
582+
if let Some((filename, error_reported)) = error_context {
583+
match writer.flush() {
584+
Ok(()) => Ok(n),
585+
Err(e) if ignorable_io_error(&e, settings) => Ok(bytes.len()),
586+
Err(e) => {
587+
if !*error_reported {
588+
uucore::show_error!("{}: {}", filename, strip_errno(&e));
589+
set_exit_code(1);
590+
*error_reported = true;
591+
}
592+
Err(io::Error::other(""))
593+
}
594+
}
595+
} else {
596+
Ok(n)
597+
}
598+
}
571599
Err(e) if ignorable_io_error(&e, settings) => Ok(bytes.len()),
572-
Err(e) => Err(e),
600+
Err(e) => {
601+
if let Some((filename, error_reported)) = error_context {
602+
if !*error_reported {
603+
uucore::show_error!("{}: {}", filename, strip_errno(&e));
604+
set_exit_code(1);
605+
*error_reported = true;
606+
}
607+
Err(io::Error::other(""))
608+
} else {
609+
Err(e)
610+
}
611+
}
573612
}
574613
}
575614

@@ -713,6 +752,12 @@ struct ByteChunkWriter<'a> {
713752

714753
/// Iterator that yields filenames for each chunk.
715754
filename_iterator: FilenameIterator<'a>,
755+
756+
/// Current filename being written to.
757+
current_filename: String,
758+
759+
/// Whether an error has already been reported for the current file.
760+
error_reported: bool,
716761
}
717762

718763
impl<'a> ByteChunkWriter<'a> {
@@ -732,10 +777,28 @@ impl<'a> ByteChunkWriter<'a> {
732777
num_chunks_written: 0,
733778
inner,
734779
filename_iterator,
780+
current_filename: filename,
781+
error_reported: false,
735782
})
736783
}
737784
}
738785

786+
impl Drop for ByteChunkWriter<'_> {
787+
fn drop(&mut self) {
788+
// Ensure final flush to catch any buffered write errors, but only report if not already reported
789+
if !self.error_reported {
790+
if let Err(e) = self.inner.flush() {
791+
uucore::show_error!(
792+
"split: {}: final flush failed: {}",
793+
self.current_filename,
794+
strip_errno(&e)
795+
);
796+
set_exit_code(1);
797+
}
798+
}
799+
}
800+
}
801+
739802
impl Write for ByteChunkWriter<'_> {
740803
/// Implements `--bytes=SIZE`
741804
fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
@@ -751,6 +814,13 @@ impl Write for ByteChunkWriter<'_> {
751814
}
752815

753816
if self.num_bytes_remaining_in_current_chunk == 0 {
817+
// Flush the current writer before switching to a new file to catch any delayed write errors
818+
if let Err(e) = self.inner.flush() {
819+
uucore::show_error!("{}: {}", self.current_filename, strip_errno(&e));
820+
set_exit_code(1);
821+
return Err(io::Error::other(""));
822+
}
823+
754824
// Increment the chunk number, reset the number of bytes remaining, and instantiate the new underlying writer.
755825
self.num_chunks_written += 1;
756826
self.num_bytes_remaining_in_current_chunk = self.chunk_size;
@@ -763,6 +833,8 @@ impl Write for ByteChunkWriter<'_> {
763833
println!("creating file {}", filename.quote());
764834
}
765835
self.inner = self.settings.instantiate_current_writer(&filename, true)?;
836+
self.current_filename = filename;
837+
self.error_reported = false;
766838
}
767839

768840
// If the capacity of this chunk is greater than the number of
@@ -771,7 +843,12 @@ impl Write for ByteChunkWriter<'_> {
771843
// the chunk number and repeat.
772844
let buf_len = buf.len();
773845
if (buf_len as u64) < self.num_bytes_remaining_in_current_chunk {
774-
let num_bytes_written = custom_write(buf, &mut self.inner, self.settings)?;
846+
let num_bytes_written = custom_write(
847+
buf,
848+
&mut self.inner,
849+
self.settings,
850+
Some((&self.current_filename, &mut self.error_reported)),
851+
)?;
775852
self.num_bytes_remaining_in_current_chunk -= num_bytes_written as u64;
776853
return Ok(carryover_bytes_written + num_bytes_written);
777854
}
@@ -782,7 +859,12 @@ impl Write for ByteChunkWriter<'_> {
782859
// self.num_bytes_remaining_in_current_chunk is lower than
783860
// n, which is already usize.
784861
let i = self.num_bytes_remaining_in_current_chunk as usize;
785-
let num_bytes_written = custom_write(&buf[..i], &mut self.inner, self.settings)?;
862+
let num_bytes_written = custom_write(
863+
&buf[..i],
864+
&mut self.inner,
865+
self.settings,
866+
Some((&self.current_filename, &mut self.error_reported)),
867+
)?;
786868
self.num_bytes_remaining_in_current_chunk -= num_bytes_written as u64;
787869

788870
// It's possible that the underlying writer did not
@@ -799,7 +881,14 @@ impl Write for ByteChunkWriter<'_> {
799881
}
800882
}
801883
fn flush(&mut self) -> io::Result<()> {
802-
self.inner.flush()
884+
match self.inner.flush() {
885+
Ok(()) => Ok(()),
886+
Err(e) => {
887+
uucore::show_error!("{}: {}", self.current_filename, strip_errno(&e));
888+
set_exit_code(1);
889+
Err(io::Error::other(""))
890+
}
891+
}
803892
}
804893
}
805894

@@ -891,7 +980,8 @@ impl Write for LineChunkWriter<'_> {
891980
// Write the line, starting from *after* the previous
892981
// separator character and ending *after* the current
893982
// separator character.
894-
let num_bytes_written = custom_write(&buf[prev..=i], &mut self.inner, self.settings)?;
983+
let num_bytes_written =
984+
custom_write(&buf[prev..=i], &mut self.inner, self.settings, None)?;
895985
total_bytes_written += num_bytes_written;
896986
prev = i + 1;
897987
self.num_lines_remaining_in_current_chunk -= 1;
@@ -907,7 +997,7 @@ impl Write for LineChunkWriter<'_> {
907997
self.num_lines_remaining_in_current_chunk = self.chunk_size;
908998
}
909999
let num_bytes_written =
910-
custom_write(&buf[prev..buf.len()], &mut self.inner, self.settings)?;
1000+
custom_write(&buf[prev..buf.len()], &mut self.inner, self.settings, None)?;
9111001
total_bytes_written += num_bytes_written;
9121002
}
9131003
Ok(total_bytes_written)
@@ -1606,7 +1696,15 @@ fn split(settings: &Settings) -> UResult<()> {
16061696
// allowable filenames, we use `ErrorKind::Other` to
16071697
// indicate that. A special error message needs to be
16081698
// printed in that case.
1609-
ErrorKind::Other => Err(USimpleError::new(1, format!("{e}"))),
1699+
ErrorKind::Other => {
1700+
let error_msg = format!("{e}");
1701+
if error_msg.is_empty() {
1702+
// This is a handled error, return error to stop processing
1703+
Err(USimpleError::new(1, ""))
1704+
} else {
1705+
Err(USimpleError::new(1, error_msg))
1706+
}
1707+
}
16101708
_ => Err(uio_error!(
16111709
e,
16121710
"{}",

tests/by-util/test_split.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,3 +2091,46 @@ fn test_split_directory_already_exists() {
20912091
.no_stdout()
20922092
.stderr_is("split: xaa: Is a directory\n");
20932093
}
2094+
2095+
#[test]
2096+
#[cfg(target_os = "linux")]
2097+
fn test_split_io_error() {
2098+
use std::process::Command;
2099+
2100+
let (at, mut ucmd) = at_and_ucmd!();
2101+
2102+
// Use ln command to create a symlink to /dev/full, like the GNU test does
2103+
// This is more reliable in various test environments
2104+
let ln_result = Command::new("ln")
2105+
.args(["-sf", "/dev/full", "xaa"])
2106+
.current_dir(at.as_string())
2107+
.status();
2108+
2109+
if ln_result.is_err() || !ln_result.unwrap().success() {
2110+
// Skip the test if we can't create the symlink (might happen in some test environments)
2111+
eprintln!("Skipping I/O error test: cannot create symlink to /dev/full");
2112+
return;
2113+
}
2114+
2115+
// Create input with 2 bytes to be split into 1-byte chunks
2116+
at.write("input", "ab");
2117+
2118+
// split should fail with exit code 1 when writing to /dev/full
2119+
ucmd.args(&["-b", "1", "input"])
2120+
.fails_with_code(1)
2121+
.no_stdout()
2122+
.stderr_contains("split: xaa: No space left on device");
2123+
2124+
// The symlink should still exist after the failed split
2125+
// Note: at.file_exists() doesn't handle symlinks properly, so we use Path::exists() as fallback
2126+
let xaa_path = at.as_string().clone() + "/xaa";
2127+
assert!(
2128+
at.file_exists("xaa") || Path::new(&xaa_path).exists(),
2129+
"Expected xaa symlink to exist after failed split"
2130+
);
2131+
// But split should not have continued to create additional files
2132+
assert!(
2133+
!at.file_exists("xab"),
2134+
"Expected xab to not exist after split failure"
2135+
);
2136+
}

0 commit comments

Comments
 (0)