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
3 changes: 1 addition & 2 deletions src/uu/shred/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
3 changes: 1 addition & 2 deletions src/uu/shred/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
67 changes: 46 additions & 21 deletions src/uu/shred/src/shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,31 @@ fn create_standard_pass_sequence(num_passes: usize) -> Vec<PassType> {
sequence
}

#[cfg(unix)]
Comment thread
NdekoCode marked this conversation as resolved.
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(
Expand All @@ -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() => {
Comment thread
NdekoCode marked this conversation as resolved.
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 {
Expand Down
52 changes: 45 additions & 7 deletions tests/by-util/test_shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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: ");
}
Loading