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
75 changes: 67 additions & 8 deletions src/uu/shred/src/shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,14 +685,27 @@ fn wipe_file(
#[cfg(not(target_os = "openbsd"))]
let get_metadata = || fs::metadata(path);

#[cfg(unix)]
let (is_block_device, is_char_device) = match get_metadata() {
Ok(md) => {
use std::os::unix::fs::FileTypeExt;
let ft = md.file_type();
(ft.is_block_device(), ft.is_char_device())
}
Err(_) => (false, false),
};

#[cfg(not(unix))]
let (is_block_device, is_char_device) = (false, false);

// `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 get_metadata() {
Ok(md) if !md.is_file() => {
Ok(md) if !md.is_file() && !is_block_device && !is_char_device => {
return Err(USimpleError::new(
1,
translate_text!("shred-not-a-file", "file" => display_path.maybe_quote()),
Expand All @@ -709,6 +722,10 @@ fn wipe_file(

let metadata = get_metadata().map_err_context(|| translate!("shred-failed-to-get-metadata"))?;

// GNU enforces --exact for devices since they have a fixed physical size
// and padding would write beyond the intended boundary.
let exact = exact || is_block_device || is_char_device;

// If force is true, set file permissions to not-readonly.
if force && !is_stdout {
let mut perms = metadata.permissions();
Expand All @@ -731,8 +748,9 @@ fn wipe_file(

// Fill up our pass sequence
let mut pass_sequence = Vec::new();
if metadata.len() != 0 {
// Only add passes if the file is non-empty
if metadata.len() != 0 || is_block_device || is_char_device {
// For regular files, only add passes if not empty. For devices, always run passes:
// block devices report len=0 via metadata, char devices write until ENOSPC

if n_passes <= 3 {
// Only random passes if n_passes <= 3
Expand Down Expand Up @@ -763,8 +781,19 @@ fn wipe_file(
|| translate_text!("shred-failed-to-open-for-writing", "file" => display_path.maybe_quote()),
)?;

let write_until_enospc = is_char_device && size.is_none();
let size = match size {
Some(size) => size,
None if is_block_device => {
//metadata.len() returns 0 for block devices on Linux;
//seeking to the end gives the real size
let len = file
.seek(SeekFrom::End(0))
.map_err_context(|| translate!("shred-failed-to-get-metadata"))?;
file.rewind()
.map_err_context(|| translate!("shred-failed-to-get-metadata"))?;
len
}
None => metadata.len(),
};

Expand All @@ -778,7 +807,16 @@ fn wipe_file(
);
}
// size is an optional argument for exactly how many bytes we want to shred
do_pass(&mut file, &pass_type, exact, random_source, size).map_err_context(
do_pass(
&mut file,
&pass_type,
exact,
random_source,
size,
is_char_device,
write_until_enospc,
)
.map_err_context(
|| translate_text!("shred-file-write-pass-failed", "file" => display_path.maybe_quote()),
)?;
}
Expand Down Expand Up @@ -822,11 +860,30 @@ fn do_pass(
exact: bool,
random_source: Option<&RefCell<File>>,
file_size: u64,
is_char_device: bool,
write_until_enospc: bool,
) -> Result<(), io::Error> {
// We might be at the end of the file due to a previous iteration, so rewind.
file.rewind()?;

let mut writer = BytesWriter::from_pass_type(pass_type, random_source)?;

// Char devices with no known size: write until ENOSPC (e.g. /dev/full).
// Char devices with --size, and all regular/block paths, fall through below.
if write_until_enospc {
loop {
let block = writer.bytes_for_pass(BLOCK_SIZE)?;
match file.write_all(block) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::StorageFull => return Ok(()),
Err(e) => return Err(e),
}
}
}

// Regular files and block devices need rewinding between passes;
// char devices don't support seeking so we skip it.
if !is_char_device {
file.rewind()?;
}

let (number_of_blocks, bytes_left) = split_on_blocks(file_size, exact);

// We start by writing BLOCK_SIZE times as many time as possible.
Expand All @@ -839,7 +896,9 @@ fn do_pass(
let block = writer.bytes_for_pass(bytes_left as usize)?;
file.write_all(block)?;

file.sync_data()?;
if !is_char_device {
file.sync_data()?;
}

Ok(())
}
Expand Down
57 changes: 57 additions & 0 deletions tests/by-util/test_shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,3 +692,60 @@ fn test_shred_dash_rejects_stdout_pipe() {
.stderr_contains("shred: -:")
.stderr_does_not_contain("/dev/fd/1");
}

// shred /dev/full should succeed (ENOSPC is a valid termination condition for devices)
#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: host device paths (/dev) not visible"
)]
fn test_shred_dev_full() {
new_ucmd!().arg("/dev/full").succeeds();
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: host device paths (/dev) not visible"
)]
fn test_shred_dev_full_with_size() {
new_ucmd!()
.args(&["-n1", "--size=1", "/dev/full"])
.fails()
.stderr_contains("No space left on device");
}

// shred /dev/null should loop until interrupted; with --size we can bound it
#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: host device paths (/dev) not visible"
)]
fn test_shred_dev_null() {
new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds();
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: host device paths (/dev) not visible"
)]
fn test_shred_dev_null_zero_size() {
new_ucmd!()
.args(&["-n1", "--size=0", "/dev/null"])
.succeeds();
}

#[test]
#[cfg(unix)]
fn test_shred_rejects_fifo() {
let (at, mut ucmd) = at_and_ucmd!();

at.mkfifo("fifo");

ucmd.arg("fifo").fails().stderr_contains("fifo");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put the whole expected error message here ?
And maybe use fails_with_code(1) to ensure we are not failing for another reason

}
Loading