diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 995862609..329bb78ba 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -41,6 +41,9 @@ windows-sys = { workspace = true, features = [ [target.'cfg(unix)'.dependencies] libc = { workspace = true } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "mv" path = "src/main.rs" diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl index 07d1155b8..9f3f5fd08 100644 --- a/src/uu/mv/locales/en-US.ftl +++ b/src/uu/mv/locales/en-US.ftl @@ -48,6 +48,8 @@ mv-help-verbose = explain what is being done mv-help-progress = Display a progress bar. Note: this feature is not supported by GNU coreutils. mv-help-debug = explain how a file is copied. Implies -v +mv-help-selinux = set SELinux security context of destination file to default type +mv-help-context = like -Z, or if CTX is specified then set the SELinux security context to CTX # Verbose messages mv-verbose-renamed = renamed {$from} -> {$to} diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 6576d9717..bcc8de2c3 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -44,6 +44,8 @@ use uucore::fs::{ }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; +#[cfg(feature = "selinux")] +use uucore::selinux::set_selinux_security_context; use uucore::translate; use uucore::update_control; @@ -99,6 +101,9 @@ pub struct Options { /// `--debug` pub debug: bool, + + /// `-Z, --context` + pub context: Option, } impl Default for Options { @@ -114,6 +119,7 @@ impl Default for Options { strip_slashes: false, progress_bar: false, debug: false, + context: None, } } } @@ -140,6 +146,8 @@ static OPT_VERBOSE: &str = "verbose"; static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; static OPT_DEBUG: &str = "debug"; +static OPT_CONTEXT: &str = "context"; +static OPT_SELINUX: &str = "selinux"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -189,6 +197,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } + // Handle -Z and --context options + // If -Z is used, use the default context (empty string) + // If --context=value is used, use that specific value + let context = if matches.get_flag(OPT_SELINUX) { + Some(String::new()) + } else { + matches.get_one::(OPT_CONTEXT).cloned() + }; + let opts = Options { overwrite: overwrite_mode, backup: backup_mode, @@ -200,6 +217,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), progress_bar: matches.get_flag(OPT_PROGRESS), debug: matches.get_flag(OPT_DEBUG), + context, }; mv(&files[..], &opts) @@ -282,6 +300,22 @@ pub fn uu_app() -> Command { .help(translate!("mv-help-progress")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_SELINUX) + .short('Z') + .help(translate!("mv-help-selinux")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(OPT_CONTEXT) + .long(OPT_CONTEXT) + .value_name("CTX") + .value_parser(clap::value_parser!(String)) + .help(translate!("mv-help-context")) + .num_args(0..=1) + .require_equals(true) + .default_missing_value(""), + ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) @@ -733,6 +767,12 @@ fn rename( rename_with_fallback(from, to, multi_progress, None, None)?; } + #[cfg(feature = "selinux")] + if let Some(ref context) = opts.context { + set_selinux_security_context(to, Some(context)) + .map_err(|e| io::Error::other(e.to_string()))?; + } + if opts.verbose { let message = match backup_path { Some(path) => { diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 62c12c1d2..00cba71b4 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -10,6 +10,11 @@ use rstest::rstest; use std::io::Write; #[cfg(not(windows))] use std::path::Path; +<<<<<<< HEAD +======= +#[cfg(feature = "selinux")] +use uucore::selinux::get_getfattr_output; +>>>>>>> 947680a6d (fix build) use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::{at_and_ucmd, util_name}; @@ -2488,3 +2493,83 @@ fn test_mv_cross_device_permission_denied() { set_permissions(other_fs_tempdir.path(), PermissionsExt::from_mode(0o755)) .expect("Unable to restore directory permissions"); } + +#[test] +#[cfg(feature = "selinux")] +fn test_mv_selinux_context() { + use std::process::Command; + + let test_cases = [ + ("-Z", None), + ( + "--context=unconfined_u:object_r:user_tmp_t:s0", + Some("unconfined_u"), + ), + ]; + + for (arg, expected_context) in test_cases { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "source.txt"; + let dest = "dest.txt"; + + at.touch(&src); + + for (arg, context_value, expected_context) in test_cases { + + let mut cmd = scene.ucmd(); + cmd.arg(arg); + + let result = cmd + .arg(at.plus_as_string(&src)) + .arg(at.plus_as_string(&dest)) + .run(); + + // Skip test if SELinux is not enabled + if result + .stderr_str() + .contains("SELinux is not enabled on this system") + { + println!("Skipping SELinux test: SELinux is not enabled"); + return; + } + + result.success(); + assert!(at.file_exists(&dest)); + assert!(!at.file_exists(&src)); + + // Verify SELinux context was set using getfattr + let getfattr_output = Command::new("getfattr") + .arg(at.plus_as_string(&dest)) + .arg("-n") + .arg("security.selinux") + .output(); + + if let Ok(output) = getfattr_output { + let selinux_context = String::from_utf8_lossy(&output.stdout); + if !selinux_context.is_empty() { + match expected_context { + Some(expected) => { + let context_value = + selinux_context.split('"').nth(1).unwrap_or("").to_string(); + assert!( + context_value.contains(expected), + "Expected context to contain '{}', got: {}", + expected, + context_value + ); + } + None => { + if selinux_context.contains("security.selinux") { + println!("SELinux context successfully set with {} flag", arg); + } + } + } + } + } + + // Clean up files + let _ = std::fs::remove_file(at.plus_as_string(&dest)); + let _ = std::fs::remove_file(at.plus_as_string(&src)); + } +}