diff --git a/src/uu/shred/locales/en-US.ftl b/src/uu/shred/locales/en-US.ftl index b2f749a0b82..234f00e24b3 100644 --- a/src/uu/shred/locales/en-US.ftl +++ b/src/uu/shred/locales/en-US.ftl @@ -41,10 +41,10 @@ shred-missing-file-operand = missing file operand shred-invalid-number-of-passes = invalid number of passes: {$passes} shred-cannot-open-random-source = cannot open random source: {$source} shred-invalid-file-size = invalid file size: {$size} -shred-no-such-file-or-directory = {$file}: No such file or directory shred-failed-to-open-for-writing-not-a-directory = {$file}: failed to open for writing: Not a directory shred-failed-to-open-for-writing-is-a-directory = {$file}: failed to open for writing: Is a directory shred-not-a-file = {$file}: Not a file +shred-invalid-file-type = {$file}: invalid file type # Option help text shred-force-help = change permissions to allow writing if necessary @@ -72,5 +72,4 @@ shred-failed-to-remove-file = {$file}: failed to remove file shred-failed-to-clone-file-handle = failed to clone file handle shred-failed-to-seek-file = failed to seek in file shred-failed-to-read-seed-bytes = failed to read seed bytes from file -shred-failed-to-get-metadata = failed to get file metadata shred-failed-to-set-permissions = failed to set file permissions diff --git a/src/uu/shred/locales/fr-FR.ftl b/src/uu/shred/locales/fr-FR.ftl index d03b9d74ad8..382739ff75d 100644 --- a/src/uu/shred/locales/fr-FR.ftl +++ b/src/uu/shred/locales/fr-FR.ftl @@ -40,10 +40,10 @@ shred-missing-file-operand = opérande de fichier manquant shred-invalid-number-of-passes = nombre de passes invalide : {$passes} shred-cannot-open-random-source = impossible d'ouvrir la source aléatoire : {$source} shred-invalid-file-size = taille de fichier invalide : {$size} -shred-no-such-file-or-directory = {$file} : Aucun fichier ou répertoire de ce type shred-failed-to-open-for-writing-not-a-directory = {$file} : impossible d'ouvrir en écriture : N'est pas un répertoire shred-failed-to-open-for-writing-is-a-directory = {$file} : impossible d'ouvrir en écriture : Est un répertoire shred-not-a-file = {$file} : N'est pas un fichier +shred-invalid-file-type = {$file} : type de fichier invalide # Texte d'aide des options shred-force-help = modifier les permissions pour permettre l'écriture si nécessaire @@ -71,5 +71,4 @@ shred-failed-to-remove-file = {$file} : impossible de supprimer le fichier shred-failed-to-clone-file-handle = échec du clonage du descripteur de fichier shred-failed-to-seek-file = échec de la recherche dans le fichier shred-failed-to-read-seed-bytes = échec de la lecture des octets de graine du fichier -shred-failed-to-get-metadata = échec de l'obtention des métadonnées du fichier shred-failed-to-set-permissions = échec de la définition des permissions du fichier diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 5d550a98edf..418e93ac21d 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -610,6 +610,31 @@ fn create_standard_pass_sequence(num_passes: usize) -> Vec { sequence } +#[cfg(unix)] +fn is_fifo(metadata: &fs::Metadata) -> bool { + use std::os::unix::fs::FileTypeExt; + metadata.file_type().is_fifo() +} + +#[cfg(not(unix))] +fn is_fifo(_metadata: &fs::Metadata) -> bool { + false +} + +/// A character or block device, which shred does not overwrite yet. +#[cfg(unix)] +fn is_special_file(metadata: &fs::Metadata) -> bool { + use std::os::unix::fs::FileTypeExt; + let file_type = metadata.file_type(); + file_type.is_char_device() || file_type.is_block_device() +} + +/// Anything but a regular file: `std` cannot tell the kinds apart here. +#[cfg(not(unix))] +fn is_special_file(metadata: &fs::Metadata) -> bool { + !metadata.is_file() +} + #[allow(clippy::too_many_arguments)] #[allow(clippy::cognitive_complexity)] fn wipe_file( @@ -626,45 +651,45 @@ fn wipe_file( // Get these potential errors out of the way first let path = Path::new(path_str); - if path_str.as_encoded_bytes().ends_with(b"/") { - if path.is_dir() { + // One stat classifies the file and later gives its size and mode. A FIFO + // has to be refused before the open below, which would block until a + // reader shows up. The stat errors (missing file, "f/" with `f` a file, + // no search permission on the parent) are the ones that open would meet, + // so they are reported the way GNU reports them: a failure to open for + // writing. The directory and "Not a directory" messages are spelled out + // because the OS text differs on Windows. + let metadata = match fs::metadata(path) { + Ok(md) if md.is_dir() => { return Err(USimpleError::new( 1, translate!("shred-failed-to-open-for-writing-is-a-directory", "file" => path.maybe_quote()), )); } - if fs::metadata(path).is_err_and(|e| e.kind() == io::ErrorKind::NotADirectory) { + Ok(md) if is_fifo(&md) => { return Err(USimpleError::new( 1, - translate!("shred-failed-to-open-for-writing-not-a-directory", "file" => path.maybe_quote()), + translate!("shred-invalid-file-type", "file" => path.maybe_quote()), )); } - } - - // `Path::exists()` and `Path::is_file()` both collapse any metadata error - // (including a permission error) into `false`, which made shred report a - // file whose parent directory lacks search permission as "No such file or - // directory". Inspect the metadata directly so a genuine `ENOENT` stays a - // "no such file" error while a permission error falls through to the - // open-for-writing below, which surfaces the real reason. - match fs::metadata(path) { - Ok(md) if !md.is_file() => { + Ok(md) if is_special_file(&md) => { return Err(USimpleError::new( 1, translate!("shred-not-a-file", "file" => path.maybe_quote()), )); } - Err(err) if err.kind() == io::ErrorKind::NotFound => { + Ok(md) => md, + Err(err) if err.kind() == io::ErrorKind::NotADirectory => { return Err(USimpleError::new( 1, - translate!("shred-no-such-file-or-directory", "file" => path.maybe_quote()), + translate!("shred-failed-to-open-for-writing-not-a-directory", "file" => path.maybe_quote()), )); } - _ => {} - } - - let metadata = - fs::metadata(path).map_err_context(|| translate!("shred-failed-to-get-metadata"))?; + Err(err) => { + return Err(err).map_err_context( + || translate!("shred-failed-to-open-for-writing", "file" => path.maybe_quote()), + ); + } + }; // If force is true, set file permissions to not-readonly. if force { diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 09cea46b439..d01003fffd0 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -502,13 +502,11 @@ fn test_shred_inaccessible_file_reports_real_error() { at.touch("locked/file"); set_permissions(at.plus_as_string("locked"), Permissions::from_mode(0o000)).unwrap(); - let result = scene.ucmd().arg("locked/file").fails(); - result.stderr_contains("Permission denied"); - assert!( - !result.stderr_str().contains("No such file"), - "shred misreported an inaccessible file as missing: {}", - result.stderr_str() - ); + scene + .ucmd() + .arg("locked/file") + .fails_with_code(1) + .stderr_only("shred: locked/file: failed to open for writing: Permission denied\n"); // Restore search permission so the fixture directory can be cleaned up. set_permissions(at.plus_as_string("locked"), Permissions::from_mode(0o755)).unwrap(); @@ -585,3 +583,43 @@ fn test_couldnt_rename() { .fails_with_code(1) .stderr_is("shred: /proc/self/mem: Couldn't rename to '/proc/self/000': No such file or directory\n"); } + +#[test] +fn test_shred_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("d"); + ucmd.arg("d") + .fails_with_code(1) + .stderr_only("shred: d: failed to open for writing: Is a directory\n"); +} + +#[test] +fn test_shred_missing_file() { + new_ucmd!() + .arg("missing") + .fails_with_code(1) + .stderr_only("shred: missing: failed to open for writing: No such file or directory\n"); +} + +#[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no FIFO or socket file types")] +fn test_shred_fifo() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("p"); + ucmd.arg("p") + .fails_with_code(1) + .stderr_only("shred: p: invalid file type\n"); +} + +#[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no FIFO or socket file types")] +fn test_shred_socket() { + // GNU reaches the open and reports its failure; so does shred here. + let (at, mut ucmd) = at_and_ucmd!(); + let _listener = std::os::unix::net::UnixListener::bind(at.plus("s")).unwrap(); + ucmd.arg("s") + .fails_with_code(1) + .stderr_contains("shred: s: failed to open for writing: "); +}