mv: implement selinux options (-Z/--context)

This commit is contained in:
Sylvestre Ledru
2025-07-31 17:45:34 +02:00
parent 7003b1d3fc
commit ad8fdbdb77
4 changed files with 130 additions and 0 deletions
+3
View File
@@ -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"
+2
View File
@@ -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}
+40
View File
@@ -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<String>,
}
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::<String>(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) => {
+85
View File
@@ -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));
}
}