From c867d6bfb1695721f1093ce967ebc21fbd847b74 Mon Sep 17 00:00:00 2001 From: Kostiantyn Hryshchuk Date: Sat, 6 Jan 2024 22:50:21 +0100 Subject: [PATCH] shred: implemented "--remove" arg (#5790) --- src/uu/shred/src/shred.rs | 105 +++++++++++++++++++++++++++--------- tests/by-util/test_shred.rs | 77 ++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 24 deletions(-) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index a77bfe5e1..b142e2e94 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -28,10 +28,17 @@ pub mod options { pub const FILE: &str = "file"; pub const ITERATIONS: &str = "iterations"; pub const SIZE: &str = "size"; + pub const WIPESYNC: &str = "u"; pub const REMOVE: &str = "remove"; pub const VERBOSE: &str = "verbose"; pub const EXACT: &str = "exact"; pub const ZERO: &str = "zero"; + + pub mod remove { + pub const UNLINK: &str = "unlink"; + pub const WIPE: &str = "wipe"; + pub const WIPESYNC: &str = "wipesync"; + } } // This block size seems to match GNU (2^16 = 65536) @@ -81,6 +88,14 @@ enum PassType { Random, } +#[derive(PartialEq, Clone, Copy)] +enum RemoveMethod { + None, // Default method. Only obfuscate the file data + Unlink, // The same as 'None' + unlink the file + Wipe, // The same as 'Unlink' + obfuscate the file name before unlink + WipeSync, // The same as 'Wipe' sync the file name changes +} + /// Iterates over all possible filenames of a certain length using NAME_CHARSET as an alphabet struct FilenameIter { // Store the indices of the letters of our filename in NAME_CHARSET @@ -219,17 +234,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => unreachable!(), }; - // TODO: implement --remove HOW - // The optional HOW parameter indicates how to remove a directory entry: - // - 'unlink' => use a standard unlink call. - // - 'wipe' => also first obfuscate bytes in the name. - // - 'wipesync' => also sync each obfuscated byte to disk. - // The default mode is 'wipesync', but note it can be expensive. - // TODO: implement --random-source + let remove_method = if matches.get_flag(options::WIPESYNC) { + RemoveMethod::WipeSync + } else if matches.contains_id(options::REMOVE) { + match matches + .get_one::(options::REMOVE) + .map(AsRef::as_ref) + { + Some(options::remove::UNLINK) => RemoveMethod::Unlink, + Some(options::remove::WIPE) => RemoveMethod::Wipe, + Some(options::remove::WIPESYNC) => RemoveMethod::WipeSync, + _ => unreachable!("should be caught by clap"), + } + } else { + RemoveMethod::None + }; + let force = matches.get_flag(options::FORCE); - let remove = matches.get_flag(options::REMOVE); let size_arg = matches .get_one::(options::SIZE) .map(|s| s.to_string()); @@ -240,7 +263,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for path_str in matches.get_many::(options::FILE).unwrap() { show_if_err!(wipe_file( - path_str, iterations, remove, size, exact, zero, verbose, force, + path_str, + iterations, + remove_method, + size, + exact, + zero, + verbose, + force, )); } Ok(()) @@ -276,12 +306,26 @@ pub fn uu_app() -> Command { .help("shred this many bytes (suffixes like K, M, G accepted)"), ) .arg( - Arg::new(options::REMOVE) + Arg::new(options::WIPESYNC) .short('u') - .long(options::REMOVE) - .help("truncate and remove file after overwriting; See below") + .help("deallocate and remove file after overwriting") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::REMOVE) + .long(options::REMOVE) + .value_name("HOW") + .value_parser([ + options::remove::UNLINK, + options::remove::WIPE, + options::remove::WIPESYNC, + ]) + .num_args(0..=1) + .require_equals(true) + .default_missing_value(options::remove::WIPESYNC) + .help("like -u but give control on HOW to delete; See below") + .action(ArgAction::Set), + ) .arg( Arg::new(options::VERBOSE) .long(options::VERBOSE) @@ -340,7 +384,7 @@ fn pass_name(pass_type: &PassType) -> String { fn wipe_file( path_str: &str, n_passes: usize, - remove: bool, + remove_method: RemoveMethod, size: Option, exact: bool, zero: bool, @@ -457,8 +501,8 @@ fn wipe_file( .map_err_context(|| format!("{}: File write pass failed", path.maybe_quote()))); } - if remove { - do_remove(path, path_str, verbose) + if remove_method != RemoveMethod::None { + do_remove(path, path_str, verbose, remove_method) .map_err_context(|| format!("{}: failed to remove file", path.maybe_quote()))?; } Ok(()) @@ -501,7 +545,7 @@ fn get_file_size(path: &Path) -> Result { // Repeatedly renames the file with strings of decreasing length (most likely all 0s) // Return the path of the file after its last renaming or None if error -fn wipe_name(orig_path: &Path, verbose: bool) -> Option { +fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Option { let file_name_len = orig_path.file_name().unwrap().to_str().unwrap().len(); let mut last_path = PathBuf::from(orig_path); @@ -526,12 +570,14 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { ); } - // Sync every file rename - let new_file = OpenOptions::new() - .write(true) - .open(new_path.clone()) - .expect("Failed to open renamed file for syncing"); - new_file.sync_all().expect("Failed to sync renamed file"); + if remove_method == RemoveMethod::WipeSync { + // Sync every file rename + let new_file = OpenOptions::new() + .write(true) + .open(new_path.clone()) + .expect("Failed to open renamed file for syncing"); + new_file.sync_all().expect("Failed to sync renamed file"); + } last_path = new_path; break; @@ -552,12 +598,23 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { Some(last_path) } -fn do_remove(path: &Path, orig_filename: &str, verbose: bool) -> Result<(), io::Error> { +fn do_remove( + path: &Path, + orig_filename: &str, + verbose: bool, + remove_method: RemoveMethod, +) -> Result<(), io::Error> { if verbose { show_error!("{}: removing", orig_filename.maybe_quote()); } - if let Some(rp) = wipe_name(path, verbose) { + let remove_path = if remove_method == RemoveMethod::Unlink { + Some(path.with_file_name(orig_filename)) + } else { + wipe_name(path, verbose, remove_method) + }; + + if let Some(rp) = remove_path { fs::remove_file(rp)?; } diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 83d2890ed..d5de7882f 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -2,6 +2,9 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +// spell-checker:ignore wipesync + use crate::common::util::TestScenario; #[test] @@ -9,8 +12,82 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_invalid_remove_arg() { + new_ucmd!().arg("--remove=unknown").fails().code_is(1); +} + +#[test] +fn test_shred() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_shred"; + let file_original_content = "test_shred file content"; + + at.write(file, file_original_content); + + ucmd.arg(file).succeeds(); + + // File exists + assert!(at.file_exists(file)); + // File is obfuscated + assert!(at.read_bytes(file) != file_original_content.as_bytes()); +} + #[test] fn test_shred_remove() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_shred_remove"; + at.touch(file); + + ucmd.arg("--remove").arg(file).succeeds(); + + // File was deleted + assert!(!at.file_exists(file)); +} + +#[test] +fn test_shred_remove_unlink() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_shred_remove_unlink"; + at.touch(file); + + ucmd.arg("--remove=unlink").arg(file).succeeds(); + + // File was deleted + assert!(!at.file_exists(file)); +} + +#[test] +fn test_shred_remove_wipe() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_shred_remove_wipe"; + at.touch(file); + + ucmd.arg("--remove=wipe").arg(file).succeeds(); + + // File was deleted + assert!(!at.file_exists(file)); +} + +#[test] +fn test_shred_remove_wipesync() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_shred_remove_wipesync"; + at.touch(file); + + ucmd.arg("--remove=wipesync").arg(file).succeeds(); + + // File was deleted + assert!(!at.file_exists(file)); +} + +#[test] +fn test_shred_u() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures;