1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

shred: implement and test feature --random-source

This commit is contained in:
Ben Wiederhake 2025-05-17 17:46:18 +02:00 committed by Sylvestre Ledru
parent 6a828f0e74
commit 5a8fad8443
2 changed files with 95 additions and 14 deletions

View file

@ -10,7 +10,7 @@ use clap::{Arg, ArgAction, Command};
use libc::S_IWUSR; use libc::S_IWUSR;
use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom}; use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom};
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Seek, Write}; use std::io::{self, Read, Seek, Write};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::prelude::PermissionsExt; use std::os::unix::prelude::PermissionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -34,6 +34,7 @@ pub mod options {
pub const VERBOSE: &str = "verbose"; pub const VERBOSE: &str = "verbose";
pub const EXACT: &str = "exact"; pub const EXACT: &str = "exact";
pub const ZERO: &str = "zero"; pub const ZERO: &str = "zero";
pub const RANDOM_SOURCE: &str = "random-source";
pub mod remove { pub mod remove {
pub const UNLINK: &str = "unlink"; pub const UNLINK: &str = "unlink";
@ -152,16 +153,25 @@ impl Iterator for FilenameIter {
} }
} }
enum RandomSource {
System,
Read(File),
}
/// Used to generate blocks of bytes of size <= BLOCK_SIZE based on either a give pattern /// Used to generate blocks of bytes of size <= BLOCK_SIZE based on either a give pattern
/// or randomness /// or randomness
// The lint warns about a large difference because StdRng is big, but the buffers are much // The lint warns about a large difference because StdRng is big, but the buffers are much
// larger anyway, so it's fine. // larger anyway, so it's fine.
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum BytesWriter { enum BytesWriter<'a> {
Random { Random {
rng: StdRng, rng: StdRng,
buffer: [u8; BLOCK_SIZE], buffer: [u8; BLOCK_SIZE],
}, },
RandomFile {
rng_file: &'a File,
buffer: [u8; BLOCK_SIZE],
},
// To write patterns we only write to the buffer once. To be able to do // To write patterns we only write to the buffer once. To be able to do
// this, we need to extend the buffer with 2 bytes. We can then easily // this, we need to extend the buffer with 2 bytes. We can then easily
// obtain a buffer starting with any character of the pattern that we // obtain a buffer starting with any character of the pattern that we
@ -177,12 +187,18 @@ enum BytesWriter {
}, },
} }
impl BytesWriter { impl<'a> BytesWriter<'a> {
fn from_pass_type(pass: &PassType) -> Self { fn from_pass_type(pass: &PassType, random_source: &'a RandomSource) -> Self {
match pass { match pass {
PassType::Random => Self::Random { PassType::Random => match random_source {
rng: StdRng::from_os_rng(), RandomSource::System => Self::Random {
buffer: [0; BLOCK_SIZE], rng: StdRng::from_os_rng(),
buffer: [0; BLOCK_SIZE],
},
RandomSource::Read(file) => Self::RandomFile {
rng_file: file,
buffer: [0; BLOCK_SIZE],
},
}, },
PassType::Pattern(pattern) => { PassType::Pattern(pattern) => {
// Copy the pattern in chunks rather than simply one byte at a time // Copy the pattern in chunks rather than simply one byte at a time
@ -203,17 +219,22 @@ impl BytesWriter {
} }
} }
fn bytes_for_pass(&mut self, size: usize) -> &[u8] { fn bytes_for_pass(&mut self, size: usize) -> Result<&[u8], io::Error> {
match self { match self {
Self::Random { rng, buffer } => { Self::Random { rng, buffer } => {
let bytes = &mut buffer[..size]; let bytes = &mut buffer[..size];
rng.fill(bytes); rng.fill(bytes);
bytes Ok(bytes)
}
Self::RandomFile { rng_file, buffer } => {
let bytes = &mut buffer[..size];
rng_file.read_exact(bytes)?;
Ok(bytes)
} }
Self::Pattern { offset, buffer } => { Self::Pattern { offset, buffer } => {
let bytes = &buffer[*offset..size + *offset]; let bytes = &buffer[*offset..size + *offset];
*offset = (*offset + size) % PATTERN_LENGTH; *offset = (*offset + size) % PATTERN_LENGTH;
bytes Ok(bytes)
} }
} }
} }
@ -240,6 +261,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
None => unreachable!(), None => unreachable!(),
}; };
let random_source = match matches.get_one::<String>(options::RANDOM_SOURCE) {
Some(filepath) => RandomSource::Read(File::open(filepath).map_err(|_| {
USimpleError::new(
1,
format!("cannot open random source: {}", filepath.quote()),
)
})?),
None => RandomSource::System,
};
// TODO: implement --random-source // TODO: implement --random-source
let remove_method = if matches.get_flag(options::WIPESYNC) { let remove_method = if matches.get_flag(options::WIPESYNC) {
@ -275,6 +305,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
size, size,
exact, exact,
zero, zero,
&random_source,
verbose, verbose,
force, force,
)); ));
@ -356,6 +387,13 @@ pub fn uu_app() -> Command {
.help("add a final overwrite with zeros to hide shredding") .help("add a final overwrite with zeros to hide shredding")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg(
Arg::new(options::RANDOM_SOURCE)
.long(options::RANDOM_SOURCE)
.help("take random bytes from FILE")
.value_hint(clap::ValueHint::FilePath)
.action(ArgAction::Set),
)
// Positional arguments // Positional arguments
.arg( .arg(
Arg::new(options::FILE) Arg::new(options::FILE)
@ -395,6 +433,7 @@ fn wipe_file(
size: Option<u64>, size: Option<u64>,
exact: bool, exact: bool,
zero: bool, zero: bool,
random_source: &RandomSource,
verbose: bool, verbose: bool,
force: bool, force: bool,
) -> UResult<()> { ) -> UResult<()> {
@ -501,7 +540,7 @@ fn wipe_file(
// size is an optional argument for exactly how many bytes we want to shred // size is an optional argument for exactly how many bytes we want to shred
// Ignore failed writes; just keep trying // Ignore failed writes; just keep trying
show_if_err!( show_if_err!(
do_pass(&mut file, &pass_type, exact, size) do_pass(&mut file, &pass_type, exact, random_source, size)
.map_err_context(|| format!("{}: File write pass failed", path.maybe_quote())) .map_err_context(|| format!("{}: File write pass failed", path.maybe_quote()))
); );
} }
@ -529,22 +568,23 @@ fn do_pass(
file: &mut File, file: &mut File,
pass_type: &PassType, pass_type: &PassType,
exact: bool, exact: bool,
random_source: &RandomSource,
file_size: u64, file_size: u64,
) -> Result<(), io::Error> { ) -> Result<(), io::Error> {
// We might be at the end of the file due to a previous iteration, so rewind. // We might be at the end of the file due to a previous iteration, so rewind.
file.rewind()?; file.rewind()?;
let mut writer = BytesWriter::from_pass_type(pass_type); let mut writer = BytesWriter::from_pass_type(pass_type, random_source);
let (number_of_blocks, bytes_left) = split_on_blocks(file_size, exact); let (number_of_blocks, bytes_left) = split_on_blocks(file_size, exact);
// We start by writing BLOCK_SIZE times as many time as possible. // We start by writing BLOCK_SIZE times as many time as possible.
for _ in 0..number_of_blocks { for _ in 0..number_of_blocks {
let block = writer.bytes_for_pass(BLOCK_SIZE); let block = writer.bytes_for_pass(BLOCK_SIZE)?;
file.write_all(block)?; file.write_all(block)?;
} }
// Then we write remaining data which is smaller than the BLOCK_SIZE // Then we write remaining data which is smaller than the BLOCK_SIZE
let block = writer.bytes_for_pass(bytes_left as usize); let block = writer.bytes_for_pass(bytes_left as usize)?;
file.write_all(block)?; file.write_all(block)?;
file.sync_data()?; file.sync_data()?;

View file

@ -251,3 +251,44 @@ fn test_all_patterns_present() {
result.stderr_contains(pat); result.stderr_contains(pat);
} }
} }
#[test]
fn test_random_source_regular_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
// Currently, our block size is 4096. If it changes, this test has to be adapted.
let mut many_bytes = Vec::with_capacity(4096 * 4);
for i in 0..4096u32 {
many_bytes.extend(i.to_le_bytes());
}
assert_eq!(many_bytes.len(), 4096 * 4);
at.write_bytes("source_long", &many_bytes);
let file = "foo.txt";
at.write(file, "a");
scene
.ucmd()
.arg("-vn3")
.arg("--random-source=source_long")
.arg(file)
.succeeds()
.stderr_only("shred: foo.txt: pass 1/3 (random)...\nshred: foo.txt: pass 2/3 (random)...\nshred: foo.txt: pass 3/3 (random)...\n");
// Should rewrite the file exactly three times
assert_eq!(at.read_bytes(file), many_bytes[(4096 * 2)..(4096 * 3)]);
}
#[test]
#[ignore = "known issue #7947"]
fn test_random_source_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("source");
let file = "foo.txt";
at.write(file, "a");
scene
.ucmd()
.arg("-v")
.arg("--random-source=source")
.arg(file)
.fails()
.stderr_only("shred: foo.txt: pass 1/3 (random)...\nshred: foo.txt: File write pass failed: Is a directory\n");
}