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:
parent
6a828f0e74
commit
5a8fad8443
2 changed files with 95 additions and 14 deletions
|
@ -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,13 +187,19 @@ 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 {
|
||||||
|
RandomSource::System => Self::Random {
|
||||||
rng: StdRng::from_os_rng(),
|
rng: StdRng::from_os_rng(),
|
||||||
buffer: [0; BLOCK_SIZE],
|
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
|
||||||
// We prefill the pattern so that the buffer can be reused at each
|
// We prefill the pattern so that the buffer can be reused at each
|
||||||
|
@ -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()?;
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue