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
5 changes: 4 additions & 1 deletion src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,10 @@ impl Options {

let recursive = matches.get_flag(options::RECURSIVE) || matches.get_flag(options::ARCHIVE);

let backup_mode = match backup_control::determine_backup_mode(matches) {
let backup_mode = match backup_control::determine_backup_mode(
std::env::var("VERSION_CONTROL").ok(),
matches,
) {
Err(e) => return Err(CpError::Backup(BackupError(format!("{e}")))),
Ok(mode) => mode,
};
Expand Down
3 changes: 2 additions & 1 deletion src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
None
};

let backup_mode = backup_control::determine_backup_mode(matches)?;
let backup_mode =
backup_control::determine_backup_mode(std::env::var("VERSION_CONTROL").ok(), matches)?;
let target_dir = matches.get_one::<String>(OPT_TARGET_DIRECTORY).cloned();
let no_target_dir = matches.get_flag(OPT_NO_TARGET_DIRECTORY);
if target_dir.is_some() && no_target_dir {
Expand Down
3 changes: 2 additions & 1 deletion src/uu/ln/src/ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
OverwriteMode::NoClobber
};

let backup_mode = backup_control::determine_backup_mode(&matches)?;
let backup_mode =
backup_control::determine_backup_mode(std::env::var("VERSION_CONTROL").ok(), &matches)?;
let backup_suffix = backup_control::determine_backup_suffix(&matches);

// When we have "-L" or "-L -P", false otherwise
Expand Down
3 changes: 2 additions & 1 deletion src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}

let overwrite_mode = determine_overwrite_mode(&matches);
let backup_mode = backup_control::determine_backup_mode(&matches)?;
let backup_mode =
backup_control::determine_backup_mode(env::var("VERSION_CONTROL").ok(), &matches)?;
let update_mode = update_control::determine_update_mode(&matches);

if backup_mode != BackupMode::None
Expand Down
109 changes: 19 additions & 90 deletions src/uucore/src/lib/features/backup_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
//! "command", "--backup=t", "--suffix=bak~"
//! ]);
//!
//! let backup_mode = match backup_control::determine_backup_mode(&matches) {
//! let backup_mode = match backup_control::determine_backup_mode(std::env::var("VERSION_CONTROL").ok(), &matches) {
//! Err(e) => {
//! show!(e);
//! return;
Expand Down Expand Up @@ -340,7 +340,7 @@ pub fn determine_backup_suffix(matches: &ArgMatches) -> String {
/// show!(err);
/// }
/// ```
pub fn determine_backup_mode(matches: &ArgMatches) -> UResult<BackupMode> {
pub fn determine_backup_mode(env_ctl: Option<String>, matches: &ArgMatches) -> UResult<BackupMode> {
if matches.contains_id(arguments::OPT_BACKUP) {
// Use method to determine the type of backups to make. When this option
// is used but method is not specified, then the value of the
Expand All @@ -349,7 +349,7 @@ pub fn determine_backup_mode(matches: &ArgMatches) -> UResult<BackupMode> {
if let Some(method) = matches.get_one::<String>(arguments::OPT_BACKUP) {
// Second argument is for the error string that is returned.
match_method(method, "backup type")
} else if let Ok(method) = env::var("VERSION_CONTROL") {
} else if let Some(method) = env_ctl {
// Second argument is for the error string that is returned.
match_method(&method, "$VERSION_CONTROL")
} else {
Expand All @@ -360,15 +360,15 @@ pub fn determine_backup_mode(matches: &ArgMatches) -> UResult<BackupMode> {
// the short form of this option, -b does not accept any argument.
// if VERSION_CONTROL is not set then using -b is equivalent to
// using --backup=existing.
if let Ok(method) = env::var("VERSION_CONTROL") {
if let Some(method) = env_ctl {
match_method(&method, "$VERSION_CONTROL")
} else {
Ok(BackupMode::Existing)
}
} else if matches.contains_id(arguments::OPT_SUFFIX) {
// Suffix option is enough to determine mode even if --backup is not set.
// If VERSION_CONTROL is not set, the default backup type is 'existing'.
if let Ok(method) = env::var("VERSION_CONTROL") {
if let Some(method) = env_ctl {
match_method(&method, "$VERSION_CONTROL")
} else {
Ok(BackupMode::Existing)
Expand Down Expand Up @@ -490,20 +490,7 @@ pub fn source_is_target_backup(source: &Path, target: &Path, suffix: &str) -> bo
#[cfg(test)]
mod tests {
use super::*;
// Required to instantiate mutex in shared context
use clap::Command;
use std::sync::Mutex;

// The mutex is required here as by default all tests are run as separate
// threads under the same parent process. As environment variables are
// specific to processes (and thus shared among threads), data races *will*
// occur if no precautions are taken. Thus we have all tests that rely on
// environment variables lock this empty mutex to ensure they don't access
// it concurrently.
static TEST_MUTEX: Mutex<()> = Mutex::new(());

// Environment variable for "VERSION_CONTROL"
static ENV_VERSION_CONTROL: &str = "VERSION_CONTROL";

fn make_app() -> Command {
Command::new("command")
Expand All @@ -515,55 +502,40 @@ mod tests {
// Defaults to --backup=existing
#[test]
fn test_backup_mode_short_only() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::Existing);
}

// --backup takes precedence over -b
#[test]
fn test_backup_mode_long_preferred_over_short() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b", "--backup=none"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::None);
}

// --backup can be passed without an argument
#[test]
fn test_backup_mode_long_without_args_no_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::Existing);
}

// --backup can be passed with an argument only
#[test]
fn test_backup_mode_long_with_args() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=simple"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::Simple);
}

// --backup errors on invalid argument
#[test]
fn test_backup_mode_long_with_args_invalid() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=foobar"]);

let result = determine_backup_mode(&matches);

let result = determine_backup_mode(None, &matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("invalid argument 'foobar' for 'backup type'"));
Expand All @@ -572,11 +544,8 @@ mod tests {
// --backup errors on ambiguous argument
#[test]
fn test_backup_mode_long_with_args_ambiguous() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=n"]);

let result = determine_backup_mode(&matches);

let result = determine_backup_mode(None, &matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("ambiguous argument 'n' for 'backup type'"));
Expand All @@ -585,122 +554,82 @@ mod tests {
// --backup accepts shortened arguments (si for simple)
#[test]
fn test_backup_mode_long_with_arg_shortened() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=si"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::Simple);
}

// -b doesn't ignores the "VERSION_CONTROL" environment variable
#[test]
fn test_backup_mode_short_does_not_ignore_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "numbered") };
let matches = make_app().get_matches_from(vec!["command", "-b"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(Some("numbered".into()), &matches).unwrap();
assert_eq!(result, BackupMode::Numbered);
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

// --backup can be passed without an argument, but reads env var if existent
#[test]
fn test_backup_mode_long_without_args_with_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "none") };
let matches = make_app().get_matches_from(vec!["command", "--backup"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(Some("none".into()), &matches).unwrap();
assert_eq!(result, BackupMode::None);
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

// --backup errors on invalid VERSION_CONTROL env var
#[test]
fn test_backup_mode_long_with_env_var_invalid() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "foobar") };
let matches = make_app().get_matches_from(vec!["command", "--backup"]);

let result = determine_backup_mode(&matches);

let result = determine_backup_mode(Some("foobar".into()), &matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("invalid argument 'foobar' for '$VERSION_CONTROL'"));
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

// --backup errors on ambiguous VERSION_CONTROL env var
#[test]
fn test_backup_mode_long_with_env_var_ambiguous() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "n") };
let matches = make_app().get_matches_from(vec!["command", "--backup"]);

let result = determine_backup_mode(&matches);

let result = determine_backup_mode(Some("n".into()), &matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("ambiguous argument 'n' for '$VERSION_CONTROL'"));
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

// --backup accepts shortened env vars (si for simple)
#[test]
fn test_backup_mode_long_with_env_var_shortened() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "si") };
let matches = make_app().get_matches_from(vec!["command", "--backup"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(Some("si".into()), &matches).unwrap();
assert_eq!(result, BackupMode::Simple);
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

// Using --suffix without --backup defaults to --backup=existing
#[test]
fn test_backup_mode_suffix_without_backup_option() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--suffix", ".bak"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(None, &matches).unwrap();
assert_eq!(result, BackupMode::Existing);
}

// Using --suffix without --backup uses env var if existing
#[test]
fn test_backup_mode_suffix_without_backup_option_with_env_var() {
let _dummy = TEST_MUTEX.lock().unwrap();
unsafe { env::set_var(ENV_VERSION_CONTROL, "numbered") };
let matches = make_app().get_matches_from(vec!["command", "--suffix", ".bak"]);

let result = determine_backup_mode(&matches).unwrap();

let result = determine_backup_mode(Some("numbered".into()), &matches).unwrap();
assert_eq!(result, BackupMode::Numbered);
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
}

#[test]
fn test_suffix_takes_hyphen_value() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b", "--suffix", "-v"]);

let result = determine_backup_suffix(&matches);
assert_eq!(result, "-v");
}

#[test]
fn test_suffix_rejects_path_traversal() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches =
make_app().get_matches_from(vec!["command", "-b", "--suffix", "_/../../dest"]);

let result = determine_backup_suffix(&matches);
assert_eq!(result, DEFAULT_BACKUP_SUFFIX);
}
Expand Down
Loading