From 3377b8bfee2e8eba8ac9364546fb558984684190 Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Tue, 28 Jul 2026 18:44:44 +0300 Subject: [PATCH 1/7] implemented runuser --- Cargo.lock | 10 + Cargo.toml | 2 + src/uu/runuser/Cargo.toml | 16 ++ src/uu/runuser/runuser.md | 8 + src/uu/runuser/src/main.rs | 1 + src/uu/runuser/src/runuser.rs | 467 ++++++++++++++++++++++++++++++++++ tests/by-util/test_runuser.rs | 71 ++++++ tests/tests.rs | 4 + 8 files changed, 579 insertions(+) create mode 100644 src/uu/runuser/Cargo.toml create mode 100644 src/uu/runuser/runuser.md create mode 100644 src/uu/runuser/src/main.rs create mode 100644 src/uu/runuser/src/runuser.rs create mode 100644 tests/by-util/test_runuser.rs diff --git a/Cargo.lock b/Cargo.lock index 3d911013..619c7792 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,6 +1563,7 @@ dependencies = [ "uu_nologin", "uu_renice", "uu_rev", + "uu_runuser", "uu_setpgid", "uu_setsid", "uu_uuidgen", @@ -1761,6 +1762,15 @@ dependencies = [ "uucore 0.2.2", ] +[[package]] +name = "uu_runuser" +version = "0.0.1" +dependencies = [ + "clap", + "nix 0.31.3", + "uucore 0.2.2", +] + [[package]] name = "uu_setpgid" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index f9129a8c..82b9eb38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ feat_common_core = [ "nologin", "renice", "rev", + "runuser", "setpgid", "setsid", "uuidgen", @@ -118,6 +119,7 @@ mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", pa nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = "src/uu/nologin" } renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } +runuser = { optional = true, version = "0.0.1", package = "uu_runuser", path = "src/uu/runuser" } setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } diff --git a/src/uu/runuser/Cargo.toml b/src/uu/runuser/Cargo.toml new file mode 100644 index 00000000..737d6027 --- /dev/null +++ b/src/uu/runuser/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "uu_runuser" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/runuser.rs" + +[[bin]] +name = "runuser" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +nix = { workspace = true, features = ["process", "user"] } diff --git a/src/uu/runuser/runuser.md b/src/uu/runuser/runuser.md new file mode 100644 index 00000000..e24a01f7 --- /dev/null +++ b/src/uu/runuser/runuser.md @@ -0,0 +1,8 @@ +# runuser + +``` +runuser [options] -u user [[--] command [argument...]] +runuser [options] [-] [user [argument...]] +``` + +run a command with substitute user and group ID diff --git a/src/uu/runuser/src/main.rs b/src/uu/runuser/src/main.rs new file mode 100644 index 00000000..13b1ad13 --- /dev/null +++ b/src/uu/runuser/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_runuser); diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs new file mode 100644 index 00000000..e544d5a1 --- /dev/null +++ b/src/uu/runuser/src/runuser.rs @@ -0,0 +1,467 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; +use std::env::var; +use std::io::Error as IOError; +use std::fs::{read_to_string}; +use std::process::{Command as RunCommand}; +use std::error::Error; +use nix::unistd::{setuid, setgid, setsid, setgroups, Uid, Gid}; +use uucore::error::{UResult, USimpleError}; +use uucore::{format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("runuser.md"); +const USAGE: &str = help_usage!("runuser.md"); + +#[cfg(target_os = "linux")] +fn get_conf(filename: &str, query: &str) +-> Result>, IOError> { + let file = read_to_string(filename)?; + let line = file + .lines() + .find(|line| line.starts_with(&format!("{}:", &query))); + + match line { + Some(line) => { + let parts: Vec = line + .split(':') + .map(|s| s.to_string()) + .collect(); + + Ok(Some(parts)) + } + None => { + Ok(None) + } + } +} + +#[cfg(target_os = "linux")] +fn get_user_info(username: &str) +-> Result, Box> { + let info = get_conf("/etc/passwd", username)?; + + match info { + Some(parts) => { + if parts.len() < 7 { + return Err( + "Invalid passwd format".into() + ); + } + let [_, _, + ref uid_str, ref gid_str, _, + ref home_dir, ref shell_path] + = parts[0..7] else { + unreachable!() + }; + let uid = uid_str.parse::()?; + let gid = gid_str.parse::()?; + + Ok( + Some(( + uid, + gid, + home_dir.to_string(), + shell_path.to_string() + )) + ) + } + None => { + Ok(None) + } + } +} + +#[cfg(target_os = "linux")] +fn get_group_info(groupname: &str) -> Result, Box> { + let info = get_conf("/etc/group", groupname)?; + + match info { + Some(parts) => { + let gid = parts + .get(2) + .ok_or("Invalid group format")? + .parse::()?; + + Ok(Some(gid)) + }, + None => { + Ok(None) + } + } +} + +#[cfg(target_os = "linux")] +fn run( + path: &str, + uid: u32, + gid: u32, + supp_gids: &Vec, + home_dir: &str, + shell_path: &str, + username: &str, + args: &Vec, + preserve_env: bool, + whitelist_env: &Vec, + is_login: bool +) -> UResult{ + const ROOT_PATH: &str = + "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; + let mut cmd = RunCommand::new(path); + + if is_login { + if preserve_env { + println!( + "--preserve-environment is ignored in case `--login` is enabled" + ); + } + cmd.env_clear(); + } + if is_login || !preserve_env { + match username { + "root" => { + cmd.env("PATH", ROOT_PATH); + }, + _ => { + cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin"); + } + } + cmd.env("HOME", home_dir); + cmd.env("USER", username); + cmd.env("LOGNAME", username); + cmd.env("SHELL", shell_path); + } + for item in whitelist_env { + if !["HOME", "USER", "LOGNAME", "SHELL", "PATH"] + .contains(&item.as_str()) { + if let Ok(val) = var(item) { + cmd.env(item, val); + } + } + } + cmd.args(args); + setgroups(supp_gids.as_slice()) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set supp Gid: {}", e) + ) + )?; + setgid(Gid::from_raw(gid)) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set Gid to {}: {}", gid, e) + ) + )?; + setuid(Uid::from_raw(uid)) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set Uid to {}: {}", uid, e) + ) + )?; + + + let status = cmd + .spawn() + .map_err(|e| + match e.kind() { + std::io::ErrorKind::NotFound => USimpleError::new( + 127, format!("command `{}` was not found", path) + ), + e => USimpleError::new( + 126, format!("Failed to spawn for process: {}", e) + ) + } + )? + .wait() + .map_err(|e| + USimpleError::new( + 126, format!("Failed to wait for process: {}", e) + ) + )?; + + Ok(status.code().unwrap_or(0)) +} + +#[cfg(target_os = "linux")] +fn sep_session() -> UResult<()> { + setsid() + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set Sid: {}", e) + ) + )?; + + Ok(()) +} + +#[cfg(target_os = "linux")] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let mut is_login = false; + let mut command_args = Vec::new(); + if *matches.get_one::("login").unwrap_or(&false) { + command_args.push("-l".to_string()); + is_login = true; + } + if *matches.get_one::("fast").unwrap_or(&false) { + command_args.push("-f".to_string()); + } + if matches.contains_id("cmd") { + let command = match matches.get_one::("session_command") { + Some(cmd) => {cmd}, + None => { + sep_session()?; + matches.get_one::("command").unwrap() + } + }; + command_args.push("-c".to_string()); + command_args.push(command.to_string()); + } + let supp_gids = if let Some(supp_groups) + = matches.get_many::("supp_group") { + let mut supp_gids = Vec::new(); + + for supp_group in supp_groups { + supp_gids.push( + Gid::from_raw( + get_group_info(&supp_group) + .map_err(|e| + USimpleError::new( + 1, + format!("Failed to get supp group info: {}", e) + ) + )? + .ok_or( + USimpleError::new( + 1, "Supp group doesn't exist" + ) + )? + ) + ); + } + + Some(supp_gids) + } else { None }; + let overwritten_gid = if let Some(overwritten_group) + = matches.get_one::("group") { + Some( + get_group_info(&overwritten_group) + .map_err(|e| { + USimpleError::new( + 1, + format!("Failed to get group info: {}", e) + ) + })? + .ok_or( + USimpleError::new( + 1, "Group doesn't exist" + ) + )? + ) + } else { None }; + let rest: Vec = matches.get_many::("rest") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect::>().to_vec(); + let (username, path, command_args) + = match matches.get_one::("user") { + Some(username) => { + match matches.contains_id("cmd") { + true => { + ( + Some(username.to_string()), + matches.get_one::("shell") + .map(|s| s.clone()), + command_args + ) + }, + false => { + if rest.is_empty() { + return Err( + USimpleError::new( + 1, "Incorrect usage" + ) + ); + } + let path = rest[0].clone(); + let args = rest[1..].to_vec(); + + (Some(username.to_string()), Some(path), args) + } + } + }, + None => { + let mut rest = rest.clone(); + let mut username: Option = None; + + if let Some(arg) = rest.first() { + if arg == "-" { + is_login = true; + command_args.push("-l".to_string()); + rest.remove(0); + } + } + if let Some(arg) = rest.first() { + username = Some(arg.to_string()); + rest.remove(0); + } + match matches.contains_id("cmd") { + true => { + ( + username, + matches.get_one::("shell") + .map(|s| s.clone()), + command_args + ) + }, + false => { + command_args.extend(rest.clone()); + + (username, None, command_args) + } + } + } + }; + let (uid, gid, home_dir, shell_path) = get_user_info( + &username + .clone() + .unwrap_or("root".to_string()) + ) + .map_err(|e| { + USimpleError::new( + 1, + format!("Failed to get user info: {}", e) + ) + })? + .ok_or( + USimpleError::new( + 1, + "User doesn't exist" + ) + )?; + let status = run( + &path.unwrap_or(shell_path.clone()), + uid, + overwritten_gid.unwrap_or(gid), + &supp_gids.unwrap_or(Vec::new()), + &home_dir, + &shell_path, + &username.unwrap_or("root".to_string()), + &command_args, + *matches.get_one::("preserve_env") + .unwrap_or(&false), + &matches.get_many::("whitelist_env") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect::>() + .to_vec(), + is_login + )?; + + if status != 0 { + return Err( + USimpleError::new( + status+128, + format!("Process exited with status code {}", status) + ) + ); + } + + Ok(()) +} + +// TODO: I haven't found if this code can actually work well +// on non-linux unix operating systems. +#[cfg(not(target_os = "linux"))] +#[uucore::main] +pub fn main(args: impl uucore::Args) -> UResult<()> { + let _matches = uu_app().try_get_matches_from(args)?; + + Err::USimpleError( + 1, "`runuser` is only available on linux" + ) +} + +pub fn uu_app() -> Command { + // TODO: to --pty yet + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .arg( + Arg::new("user") + .short('u') + .long("user") + .value_name("user") + ) + .arg( + Arg::new("preserve_env") + .short('p') + .long("preserve-environment") + .visible_short_alias('m') + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("whitelist_env") + .short('w') + .long("whitelist-environment") + .value_name("list") + .num_args(0..) + ) + .arg( + Arg::new("group") + .short('g') + .long("group") + .value_name("group") + ) + .arg( + Arg::new("supp_group") + .short('F') + .long("supp-group") + .value_name("supp-group") + ) + .arg( + Arg::new("login") + .short('l') + .long("login") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("command") + .short('c') + .long("command") + .value_name("command") + ) + .arg( + Arg::new("session_command") + .long("session-command") + .value_name("command") + ) + .group( + ArgGroup::new("cmd") + .args(["command", "session_command"]) + ) + .arg( + Arg::new("fast") + .short('f') + .long("fast") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("shell") + .short('s') + .long("shell") + .value_name("shell") + ) + .arg( + Arg::new("rest") + .num_args(0..) + .hide(true) + .allow_hyphen_values(true) + .trailing_var_arg(true) + ) +} diff --git a/tests/by-util/test_runuser.rs b/tests/by-util/test_runuser.rs new file mode 100644 index 00000000..93024b0f --- /dev/null +++ b/tests/by-util/test_runuser.rs @@ -0,0 +1,71 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use uutests::new_ucmd; + +#[test] +#[cfg(target_os = "linux")] +fn test_invalid_arg() { + new_ucmd!() + .arg("--definitely-invalid") + .fails() + .code_is(1); +} + +#[test] +#[cfg(target_os = "linux")] +fn invalid_user() { + new_ucmd!() + .arg("--user=fools_have_this_username") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("User doesn't exist"); +} + +#[test] +#[cfg(target_os = "linux")] +fn invalid_group() { + new_ucmd!() + .arg("--user=root") + .arg("--group=hopefully_nonexistant_group") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("Group doesn't exist"); +} + + +#[test] +#[cfg(target_os = "linux")] +fn invalid_supp_group() { + new_ucmd!() + .arg("--user=root") + .arg("--supp-group=does_anyone_read_this") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("Supp group doesn't exist"); +} + +#[test] +#[cfg(target_os = "linux")] +fn missing_command() { + new_ucmd!() + .arg("--user=root") + .fails() + .code_is(1) + .stderr_contains("Incorrect usage"); +} + + + +#[cfg(not(target_os = "linux"))] +fn unsupported_platform () { + new_ucmd!() + .fails() + .code_is(1) + .stderr_contains("`runuser` is only available on linux") +} diff --git a/tests/tests.rs b/tests/tests.rs index 09ffc245..c8fe4f93 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -63,6 +63,10 @@ mod test_renice; #[path = "by-util/test_rev.rs"] mod test_rev; +#[cfg(feature = "runuser")] +#[path = "by-util/test_runuser.rs"] +mod test_runuser; + #[cfg(feature = "setpgid")] #[path = "by-util/test_setpgid.rs"] mod test_setpgid; From ec91840dfa4b8531dbb8de2aebfbe3e3910f8067 Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 08:11:32 +0300 Subject: [PATCH 2/7] fixed clippy warnings and improved code --- src/uu/runuser/src/runuser.rs | 158 ++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 64 deletions(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index e544d5a1..2e99a544 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -9,9 +9,10 @@ use std::io::Error as IOError; use std::fs::{read_to_string}; use std::process::{Command as RunCommand}; use std::error::Error; -use nix::unistd::{setuid, setgid, setsid, setgroups, Uid, Gid}; use uucore::error::{UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage}; +#[cfg(target_os = "linux")] +use nix::unistd::{setuid, setgid, setsid, setgroups, Uid, Gid}; const ABOUT: &str = help_about!("runuser.md"); const USAGE: &str = help_usage!("runuser.md"); @@ -22,7 +23,7 @@ fn get_conf(filename: &str, query: &str) let file = read_to_string(filename)?; let line = file .lines() - .find(|line| line.starts_with(&format!("{}:", &query))); + .find(|line| line.starts_with(&format!("{}:", query))); match line { Some(line) => { @@ -39,9 +40,16 @@ fn get_conf(filename: &str, query: &str) } } +struct UserEntry { + uid: u32, + gid: u32, + home: String, + shell: String, +} + #[cfg(target_os = "linux")] fn get_user_info(username: &str) --> Result, Box> { +-> Result, Box> { let info = get_conf("/etc/passwd", username)?; match info { @@ -61,12 +69,14 @@ fn get_user_info(username: &str) let gid = gid_str.parse::()?; Ok( - Some(( - uid, - gid, - home_dir.to_string(), - shell_path.to_string() - )) + Some( + UserEntry { + uid, + gid, + home: home_dir.to_string(), + shell: shell_path.to_string() + } + ) ) } None => { @@ -95,23 +105,45 @@ fn get_group_info(groupname: &str) -> Result, Box> { } #[cfg(target_os = "linux")] -fn run( - path: &str, - uid: u32, - gid: u32, - supp_gids: &Vec, +fn set_ids( + uid: u32, + gid: u32, + supp_gids: &Vec +) -> UResult<()> { + setgroups(supp_gids.as_slice()) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set supp Gid: {}", e) + ) + )?; + setgid(Gid::from_raw(gid)) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set Gid to {}: {}", gid, e) + ) + )?; + setuid(Uid::from_raw(uid)) + .map_err(|e| + USimpleError::new( + 1, format!("Failed to set Uid to {}: {}", uid, e) + ) + )?; + + Ok(()) +} + +fn prepare_env( + cmd: &mut RunCommand, home_dir: &str, shell_path: &str, username: &str, - args: &Vec, - preserve_env: bool, - whitelist_env: &Vec, - is_login: bool -) -> UResult{ + preserve_env: bool, + whitelist_env: &Vec, + is_login: bool +) { const ROOT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; - let mut cmd = RunCommand::new(path); - + if is_login { if preserve_env { println!( @@ -142,25 +174,13 @@ fn run( } } } - cmd.args(args); - setgroups(supp_gids.as_slice()) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set supp Gid: {}", e) - ) - )?; - setgid(Gid::from_raw(gid)) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set Gid to {}: {}", gid, e) - ) - )?; - setuid(Uid::from_raw(uid)) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set Uid to {}: {}", uid, e) - ) - )?; +} + +#[cfg(target_os = "linux")] +fn run( + cmd: &mut RunCommand +) -> UResult{ + let status = cmd @@ -168,7 +188,7 @@ fn run( .map_err(|e| match e.kind() { std::io::ErrorKind::NotFound => USimpleError::new( - 127, format!("command `{}` was not found", path) + 127, "command was not found" ), e => USimpleError::new( 126, format!("Failed to spawn for process: {}", e) @@ -228,7 +248,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for supp_group in supp_groups { supp_gids.push( Gid::from_raw( - get_group_info(&supp_group) + get_group_info(supp_group) .map_err(|e| USimpleError::new( 1, @@ -249,7 +269,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let overwritten_gid = if let Some(overwritten_group) = matches.get_one::("group") { Some( - get_group_info(&overwritten_group) + get_group_info(overwritten_group) .map_err(|e| { USimpleError::new( 1, @@ -275,7 +295,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ( Some(username.to_string()), matches.get_one::("shell") - .map(|s| s.clone()), + .cloned(), command_args ) }, @@ -314,7 +334,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ( username, matches.get_one::("shell") - .map(|s| s.clone()), + .cloned(), command_args ) }, @@ -326,7 +346,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } }; - let (uid, gid, home_dir, shell_path) = get_user_info( + let user_info = get_user_info( &username .clone() .unwrap_or("root".to_string()) @@ -343,23 +363,31 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { "User doesn't exist" ) )?; + + set_ids( + user_info.uid, + overwritten_gid.unwrap_or(user_info.gid), + &supp_gids.unwrap_or(Vec::new()) + )?; + + let mut cmd = RunCommand::new(path.unwrap_or(user_info.shell.clone())); + cmd.args(&command_args); + prepare_env( + &mut cmd, + &user_info.home, + &user_info.shell, + &username.unwrap_or("root".to_string()), + *matches.get_one::("preserve_env") + .unwrap_or(&false), + &matches.get_many::("whitelist_env") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect::>() + .to_vec(), + is_login + ); let status = run( - &path.unwrap_or(shell_path.clone()), - uid, - overwritten_gid.unwrap_or(gid), - &supp_gids.unwrap_or(Vec::new()), - &home_dir, - &shell_path, - &username.unwrap_or("root".to_string()), - &command_args, - *matches.get_one::("preserve_env") - .unwrap_or(&false), - &matches.get_many::("whitelist_env") - .unwrap_or_default() - .map(|s| s.to_string()) - .collect::>() - .to_vec(), - is_login + &mut cmd )?; if status != 0 { @@ -381,8 +409,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn main(args: impl uucore::Args) -> UResult<()> { let _matches = uu_app().try_get_matches_from(args)?; - Err::USimpleError( - 1, "`runuser` is only available on linux" + Err( + USimpleError::new( + 1, "`runuser` is only available on linux" + ) ) } From 0d76d70b3cd2faf4481c41ca33aa78d10483576c Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 08:17:55 +0300 Subject: [PATCH 3/7] formatted code --- src/uu/runuser/src/runuser.rs | 605 ++++++++++++++-------------------- tests/by-util/test_runuser.rs | 68 ++-- 2 files changed, 274 insertions(+), 399 deletions(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index 2e99a544..709b6114 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -4,22 +4,21 @@ // file that was distributed with this source code. use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; +#[cfg(target_os = "linux")] +use nix::unistd::{setgid, setgroups, setsid, setuid, Gid, Uid}; use std::env::var; -use std::io::Error as IOError; -use std::fs::{read_to_string}; -use std::process::{Command as RunCommand}; use std::error::Error; +use std::fs::read_to_string; +use std::io::Error as IOError; +use std::process::Command as RunCommand; use uucore::error::{UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage}; -#[cfg(target_os = "linux")] -use nix::unistd::{setuid, setgid, setsid, setgroups, Uid, Gid}; const ABOUT: &str = help_about!("runuser.md"); const USAGE: &str = help_usage!("runuser.md"); #[cfg(target_os = "linux")] -fn get_conf(filename: &str, query: &str) --> Result>, IOError> { +fn get_conf(filename: &str, query: &str) -> Result>, IOError> { let file = read_to_string(filename)?; let line = file .lines() @@ -27,61 +26,45 @@ fn get_conf(filename: &str, query: &str) match line { Some(line) => { - let parts: Vec = line - .split(':') - .map(|s| s.to_string()) - .collect(); + let parts: Vec = line.split(':').map(|s| s.to_string()).collect(); Ok(Some(parts)) } - None => { - Ok(None) - } + None => Ok(None), } } struct UserEntry { - uid: u32, - gid: u32, - home: String, - shell: String, + uid: u32, + gid: u32, + home: String, + shell: String, } #[cfg(target_os = "linux")] -fn get_user_info(username: &str) --> Result, Box> { +fn get_user_info(username: &str) -> Result, Box> { let info = get_conf("/etc/passwd", username)?; match info { Some(parts) => { if parts.len() < 7 { - return Err( - "Invalid passwd format".into() - ); + return Err("Invalid passwd format".into()); } - let [_, _, - ref uid_str, ref gid_str, _, - ref home_dir, ref shell_path] - = parts[0..7] else { + let [_, _, ref uid_str, ref gid_str, _, ref home_dir, ref shell_path] = parts[0..7] + else { unreachable!() }; let uid = uid_str.parse::()?; let gid = gid_str.parse::()?; - Ok( - Some( - UserEntry { - uid, - gid, - home: home_dir.to_string(), - shell: shell_path.to_string() - } - ) - ) - } - None => { - Ok(None) + Ok(Some(UserEntry { + uid, + gid, + home: home_dir.to_string(), + shell: shell_path.to_string(), + })) } + None => Ok(None), } } @@ -91,130 +74,85 @@ fn get_group_info(groupname: &str) -> Result, Box> { match info { Some(parts) => { - let gid = parts - .get(2) - .ok_or("Invalid group format")? - .parse::()?; + let gid = parts.get(2).ok_or("Invalid group format")?.parse::()?; Ok(Some(gid)) - }, - None => { - Ok(None) } + None => Ok(None), } } #[cfg(target_os = "linux")] -fn set_ids( - uid: u32, - gid: u32, - supp_gids: &Vec -) -> UResult<()> { - setgroups(supp_gids.as_slice()) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set supp Gid: {}", e) - ) - )?; - setgid(Gid::from_raw(gid)) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set Gid to {}: {}", gid, e) - ) - )?; - setuid(Uid::from_raw(uid)) - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set Uid to {}: {}", uid, e) - ) - )?; - - Ok(()) +fn set_ids(uid: u32, gid: u32, supp_gids: &Vec) -> UResult<()> { + setgroups(supp_gids.as_slice()) + .map_err(|e| USimpleError::new(1, format!("Failed to set supp Gid: {}", e)))?; + setgid(Gid::from_raw(gid)) + .map_err(|e| USimpleError::new(1, format!("Failed to set Gid to {}: {}", gid, e)))?; + setuid(Uid::from_raw(uid)) + .map_err(|e| USimpleError::new(1, format!("Failed to set Uid to {}: {}", uid, e)))?; + + Ok(()) } fn prepare_env( - cmd: &mut RunCommand, + cmd: &mut RunCommand, home_dir: &str, shell_path: &str, username: &str, - preserve_env: bool, - whitelist_env: &Vec, - is_login: bool + preserve_env: bool, + whitelist_env: &Vec, + is_login: bool, ) { - const ROOT_PATH: &str = - "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; - - if is_login { + const ROOT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; + + if is_login { if preserve_env { - println!( - "--preserve-environment is ignored in case `--login` is enabled" - ); + println!("--preserve-environment is ignored in case `--login` is enabled"); } cmd.env_clear(); - } + } if is_login || !preserve_env { - match username { - "root" => { - cmd.env("PATH", ROOT_PATH); - }, - _ => { - cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin"); - } - } - cmd.env("HOME", home_dir); - cmd.env("USER", username); - cmd.env("LOGNAME", username); - cmd.env("SHELL", shell_path); + match username { + "root" => { + cmd.env("PATH", ROOT_PATH); + } + _ => { + cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin"); + } + } + cmd.env("HOME", home_dir); + cmd.env("USER", username); + cmd.env("LOGNAME", username); + cmd.env("SHELL", shell_path); } for item in whitelist_env { - if !["HOME", "USER", "LOGNAME", "SHELL", "PATH"] - .contains(&item.as_str()) { - if let Ok(val) = var(item) { - cmd.env(item, val); - } + if !["HOME", "USER", "LOGNAME", "SHELL", "PATH"].contains(&item.as_str()) { + if let Ok(val) = var(item) { + cmd.env(item, val); + } } } } #[cfg(target_os = "linux")] -fn run( - cmd: &mut RunCommand -) -> UResult{ - - - - let status = cmd - .spawn() - .map_err(|e| - match e.kind() { - std::io::ErrorKind::NotFound => USimpleError::new( - 127, "command was not found" - ), - e => USimpleError::new( - 126, format!("Failed to spawn for process: {}", e) - ) - } - )? - .wait() - .map_err(|e| - USimpleError::new( - 126, format!("Failed to wait for process: {}", e) - ) - )?; - - Ok(status.code().unwrap_or(0)) +fn run(cmd: &mut RunCommand) -> UResult { + let status = cmd + .spawn() + .map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => USimpleError::new(127, "command was not found"), + e => USimpleError::new(126, format!("Failed to spawn for process: {}", e)), + })? + .wait() + .map_err(|e| USimpleError::new(126, format!("Failed to wait for process: {}", e)))?; + + Ok(status.code().unwrap_or(0)) } #[cfg(target_os = "linux")] fn sep_session() -> UResult<()> { - setsid() - .map_err(|e| - USimpleError::new( - 1, format!("Failed to set Sid: {}", e) - ) - )?; - - Ok(()) + setsid().map_err(|e| USimpleError::new(1, format!("Failed to set Sid: {}", e)))?; + + Ok(()) } #[cfg(target_os = "linux")] @@ -222,276 +160,219 @@ fn sep_session() -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; let mut is_login = false; - let mut command_args = Vec::new(); - if *matches.get_one::("login").unwrap_or(&false) { - command_args.push("-l".to_string()); - is_login = true; - } - if *matches.get_one::("fast").unwrap_or(&false) { - command_args.push("-f".to_string()); - } - if matches.contains_id("cmd") { - let command = match matches.get_one::("session_command") { - Some(cmd) => {cmd}, - None => { - sep_session()?; - matches.get_one::("command").unwrap() - } - }; - command_args.push("-c".to_string()); - command_args.push(command.to_string()); - } - let supp_gids = if let Some(supp_groups) - = matches.get_many::("supp_group") { + let mut command_args = Vec::new(); + if *matches.get_one::("login").unwrap_or(&false) { + command_args.push("-l".to_string()); + is_login = true; + } + if *matches.get_one::("fast").unwrap_or(&false) { + command_args.push("-f".to_string()); + } + if matches.contains_id("cmd") { + let command = match matches.get_one::("session_command") { + Some(cmd) => cmd, + None => { + sep_session()?; + matches.get_one::("command").unwrap() + } + }; + command_args.push("-c".to_string()); + command_args.push(command.to_string()); + } + let supp_gids = if let Some(supp_groups) = matches.get_many::("supp_group") { let mut supp_gids = Vec::new(); for supp_group in supp_groups { - supp_gids.push( - Gid::from_raw( - get_group_info(supp_group) - .map_err(|e| - USimpleError::new( - 1, - format!("Failed to get supp group info: {}", e) - ) - )? - .ok_or( - USimpleError::new( - 1, "Supp group doesn't exist" - ) - )? - ) - ); + supp_gids.push(Gid::from_raw( + get_group_info(supp_group) + .map_err(|e| { + USimpleError::new(1, format!("Failed to get supp group info: {}", e)) + })? + .ok_or(USimpleError::new(1, "Supp group doesn't exist"))?, + )); } Some(supp_gids) - } else { None }; - let overwritten_gid = if let Some(overwritten_group) - = matches.get_one::("group") { + } else { + None + }; + let overwritten_gid = if let Some(overwritten_group) = matches.get_one::("group") { Some( get_group_info(overwritten_group) - .map_err(|e| { - USimpleError::new( - 1, - format!("Failed to get group info: {}", e) - ) - })? - .ok_or( - USimpleError::new( - 1, "Group doesn't exist" - ) - )? + .map_err(|e| USimpleError::new(1, format!("Failed to get group info: {}", e)))? + .ok_or(USimpleError::new(1, "Group doesn't exist"))?, ) - } else { None }; - let rest: Vec = matches.get_many::("rest") - .unwrap_or_default() - .map(|s| s.to_string()) - .collect::>().to_vec(); - let (username, path, command_args) - = match matches.get_one::("user") { - Some(username) => { - match matches.contains_id("cmd") { - true => { - ( - Some(username.to_string()), - matches.get_one::("shell") - .cloned(), - command_args - ) - }, - false => { - if rest.is_empty() { - return Err( - USimpleError::new( - 1, "Incorrect usage" - ) - ); - } - let path = rest[0].clone(); - let args = rest[1..].to_vec(); - - (Some(username.to_string()), Some(path), args) - } - } + } else { + None + }; + let rest: Vec = matches + .get_many::("rest") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect::>() + .to_vec(); + let (username, path, command_args) = match matches.get_one::("user") { + Some(username) => match matches.contains_id("cmd") { + true => ( + Some(username.to_string()), + matches.get_one::("shell").cloned(), + command_args, + ), + false => { + if rest.is_empty() { + return Err(USimpleError::new(1, "Incorrect usage")); + } + let path = rest[0].clone(); + let args = rest[1..].to_vec(); + + (Some(username.to_string()), Some(path), args) + } }, None => { - let mut rest = rest.clone(); + let mut rest = rest.clone(); let mut username: Option = None; if let Some(arg) = rest.first() { - if arg == "-" { - is_login = true; - command_args.push("-l".to_string()); - rest.remove(0); - } + if arg == "-" { + is_login = true; + command_args.push("-l".to_string()); + rest.remove(0); + } } if let Some(arg) = rest.first() { - username = Some(arg.to_string()); - rest.remove(0); + username = Some(arg.to_string()); + rest.remove(0); + } + match matches.contains_id("cmd") { + true => ( + username, + matches.get_one::("shell").cloned(), + command_args, + ), + false => { + command_args.extend(rest.clone()); + + (username, None, command_args) + } } - match matches.contains_id("cmd") { - true => { - ( - username, - matches.get_one::("shell") - .cloned(), - command_args - ) - }, - false => { - command_args.extend(rest.clone()); - - (username, None, command_args) - } - } } }; - let user_info = get_user_info( - &username - .clone() - .unwrap_or("root".to_string()) - ) - .map_err(|e| { - USimpleError::new( - 1, - format!("Failed to get user info: {}", e) - ) - })? - .ok_or( - USimpleError::new( - 1, - "User doesn't exist" - ) - )?; - - set_ids( - user_info.uid, - overwritten_gid.unwrap_or(user_info.gid), - &supp_gids.unwrap_or(Vec::new()) - )?; - - let mut cmd = RunCommand::new(path.unwrap_or(user_info.shell.clone())); - cmd.args(&command_args); - prepare_env( - &mut cmd, - &user_info.home, - &user_info.shell, - &username.unwrap_or("root".to_string()), - *matches.get_one::("preserve_env") - .unwrap_or(&false), - &matches.get_many::("whitelist_env") - .unwrap_or_default() - .map(|s| s.to_string()) - .collect::>() - .to_vec(), - is_login - ); - let status = run( - &mut cmd + let user_info = get_user_info(&username.clone().unwrap_or("root".to_string())) + .map_err(|e| USimpleError::new(1, format!("Failed to get user info: {}", e)))? + .ok_or(USimpleError::new(1, "User doesn't exist"))?; + + set_ids( + user_info.uid, + overwritten_gid.unwrap_or(user_info.gid), + &supp_gids.unwrap_or(Vec::new()), )?; - + + let mut cmd = RunCommand::new(path.unwrap_or(user_info.shell.clone())); + cmd.args(&command_args); + prepare_env( + &mut cmd, + &user_info.home, + &user_info.shell, + &username.unwrap_or("root".to_string()), + *matches.get_one::("preserve_env").unwrap_or(&false), + &matches + .get_many::("whitelist_env") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect::>() + .to_vec(), + is_login, + ); + let status = run(&mut cmd)?; + if status != 0 { - return Err( - USimpleError::new( - status+128, - format!("Process exited with status code {}", status) - ) - ); + return Err(USimpleError::new( + status + 128, + format!("Process exited with status code {}", status), + )); } Ok(()) } -// TODO: I haven't found if this code can actually work well +// TODO: I haven't found if this code can actually work well // on non-linux unix operating systems. #[cfg(not(target_os = "linux"))] #[uucore::main] pub fn main(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from(args)?; + let _matches = uu_app().try_get_matches_from(args)?; - Err( - USimpleError::new( - 1, "`runuser` is only available on linux" - ) - ) + Err(USimpleError::new(1, "`runuser` is only available on linux")) } pub fn uu_app() -> Command { - // TODO: to --pty yet + // TODO: to --pty yet Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .arg(Arg::new("user").short('u').long("user").value_name("user")) + .arg( + Arg::new("preserve_env") + .short('p') + .long("preserve-environment") + .visible_short_alias('m') + .action(ArgAction::SetTrue), + ) .arg( - Arg::new("user") - .short('u') - .long("user") - .value_name("user") + Arg::new("whitelist_env") + .short('w') + .long("whitelist-environment") + .value_name("list") + .num_args(0..), ) .arg( - Arg::new("preserve_env") - .short('p') - .long("preserve-environment") - .visible_short_alias('m') - .action(ArgAction::SetTrue) + Arg::new("group") + .short('g') + .long("group") + .value_name("group"), ) .arg( - Arg::new("whitelist_env") - .short('w') - .long("whitelist-environment") - .value_name("list") - .num_args(0..) + Arg::new("supp_group") + .short('F') + .long("supp-group") + .value_name("supp-group"), ) .arg( - Arg::new("group") - .short('g') - .long("group") - .value_name("group") - ) - .arg( - Arg::new("supp_group") - .short('F') - .long("supp-group") - .value_name("supp-group") - ) - .arg( - Arg::new("login") - .short('l') - .long("login") - .action(ArgAction::SetTrue) - ) - .arg( - Arg::new("command") - .short('c') - .long("command") - .value_name("command") - ) - .arg( - Arg::new("session_command") - .long("session-command") - .value_name("command") - ) - .group( - ArgGroup::new("cmd") - .args(["command", "session_command"]) - ) - .arg( - Arg::new("fast") - .short('f') - .long("fast") - .action(ArgAction::SetTrue) - ) - .arg( - Arg::new("shell") - .short('s') - .long("shell") - .value_name("shell") - ) - .arg( - Arg::new("rest") - .num_args(0..) - .hide(true) - .allow_hyphen_values(true) - .trailing_var_arg(true) - ) + Arg::new("login") + .short('l') + .long("login") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("command") + .short('c') + .long("command") + .value_name("command"), + ) + .arg( + Arg::new("session_command") + .long("session-command") + .value_name("command"), + ) + .group(ArgGroup::new("cmd").args(["command", "session_command"])) + .arg( + Arg::new("fast") + .short('f') + .long("fast") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("shell") + .short('s') + .long("shell") + .value_name("shell"), + ) + .arg( + Arg::new("rest") + .num_args(0..) + .hide(true) + .allow_hyphen_values(true) + .trailing_var_arg(true), + ) } diff --git a/tests/by-util/test_runuser.rs b/tests/by-util/test_runuser.rs index 93024b0f..273c1ecd 100644 --- a/tests/by-util/test_runuser.rs +++ b/tests/by-util/test_runuser.rs @@ -8,64 +8,58 @@ use uutests::new_ucmd; #[test] #[cfg(target_os = "linux")] fn test_invalid_arg() { - new_ucmd!() - .arg("--definitely-invalid") - .fails() - .code_is(1); + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } #[test] #[cfg(target_os = "linux")] fn invalid_user() { - new_ucmd!() - .arg("--user=fools_have_this_username") - .arg("--command=\"echo hello_world\"") - .fails() - .code_is(1) - .stderr_contains("User doesn't exist"); + new_ucmd!() + .arg("--user=fools_have_this_username") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("User doesn't exist"); } #[test] #[cfg(target_os = "linux")] fn invalid_group() { - new_ucmd!() - .arg("--user=root") - .arg("--group=hopefully_nonexistant_group") - .arg("--command=\"echo hello_world\"") - .fails() - .code_is(1) - .stderr_contains("Group doesn't exist"); + new_ucmd!() + .arg("--user=root") + .arg("--group=hopefully_nonexistant_group") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("Group doesn't exist"); } - #[test] #[cfg(target_os = "linux")] fn invalid_supp_group() { - new_ucmd!() - .arg("--user=root") - .arg("--supp-group=does_anyone_read_this") - .arg("--command=\"echo hello_world\"") - .fails() - .code_is(1) - .stderr_contains("Supp group doesn't exist"); + new_ucmd!() + .arg("--user=root") + .arg("--supp-group=does_anyone_read_this") + .arg("--command=\"echo hello_world\"") + .fails() + .code_is(1) + .stderr_contains("Supp group doesn't exist"); } #[test] #[cfg(target_os = "linux")] fn missing_command() { - new_ucmd!() - .arg("--user=root") - .fails() - .code_is(1) - .stderr_contains("Incorrect usage"); + new_ucmd!() + .arg("--user=root") + .fails() + .code_is(1) + .stderr_contains("Incorrect usage"); } - - #[cfg(not(target_os = "linux"))] -fn unsupported_platform () { - new_ucmd!() - .fails() - .code_is(1) - .stderr_contains("`runuser` is only available on linux") +fn unsupported_platform() { + new_ucmd!() + .fails() + .code_is(1) + .stderr_contains("`runuser` is only available on linux") } From bc03b2298d2a9bfb203342d3cb10d7d004d272d3 Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 10:18:18 +0300 Subject: [PATCH 4/7] seperated functions using mod --- src/uu/runuser/src/runuser.rs | 246 ++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 118 deletions(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index 709b6114..9ee4c2ba 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -4,13 +4,6 @@ // file that was distributed with this source code. use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; -#[cfg(target_os = "linux")] -use nix::unistd::{setgid, setgroups, setsid, setuid, Gid, Uid}; -use std::env::var; -use std::error::Error; -use std::fs::read_to_string; -use std::io::Error as IOError; -use std::process::Command as RunCommand; use uucore::error::{UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage}; @@ -18,146 +11,160 @@ const ABOUT: &str = help_about!("runuser.md"); const USAGE: &str = help_usage!("runuser.md"); #[cfg(target_os = "linux")] -fn get_conf(filename: &str, query: &str) -> Result>, IOError> { - let file = read_to_string(filename)?; - let line = file - .lines() - .find(|line| line.starts_with(&format!("{}:", query))); - - match line { - Some(line) => { - let parts: Vec = line.split(':').map(|s| s.to_string()).collect(); - - Ok(Some(parts)) +mod linux { + use nix::unistd::{setgid, setgroups, setsid, setuid, Gid, Uid}; + use std::env::var; + use std::error::Error; + use std::fs::read_to_string; + use std::io::Error as IOError; + use std::process::Command as RunCommand; + use uucore::error::{UResult, USimpleError}; + + pub fn get_conf(filename: &str, query: &str) -> Result>, IOError> { + let file = read_to_string(filename)?; + let line = file + .lines() + .find(|line| line.starts_with(&format!("{}:", query))); + + match line { + Some(line) => { + let parts: Vec = line.split(':').map(|s| s.to_string()).collect(); + + Ok(Some(parts)) + } + None => Ok(None), } - None => Ok(None), } -} -struct UserEntry { - uid: u32, - gid: u32, - home: String, - shell: String, -} + pub struct UserEntry { + pub uid: u32, + pub gid: u32, + pub home: String, + pub shell: String, + } -#[cfg(target_os = "linux")] -fn get_user_info(username: &str) -> Result, Box> { - let info = get_conf("/etc/passwd", username)?; + pub fn get_user_info(username: &str) -> Result, Box> { + let info = get_conf("/etc/passwd", username)?; - match info { - Some(parts) => { - if parts.len() < 7 { - return Err("Invalid passwd format".into()); + match info { + Some(parts) => { + if parts.len() < 7 { + return Err("Invalid passwd format".into()); + } + let [_, _, ref uid_str, ref gid_str, _, ref home_dir, ref shell_path] = parts[0..7] + else { + unreachable!() + }; + let uid = uid_str.parse::()?; + let gid = gid_str.parse::()?; + + Ok(Some(UserEntry { + uid, + gid, + home: home_dir.to_string(), + shell: shell_path.to_string(), + })) } - let [_, _, ref uid_str, ref gid_str, _, ref home_dir, ref shell_path] = parts[0..7] - else { - unreachable!() - }; - let uid = uid_str.parse::()?; - let gid = gid_str.parse::()?; - - Ok(Some(UserEntry { - uid, - gid, - home: home_dir.to_string(), - shell: shell_path.to_string(), - })) + None => Ok(None), } - None => Ok(None), } -} -#[cfg(target_os = "linux")] -fn get_group_info(groupname: &str) -> Result, Box> { - let info = get_conf("/etc/group", groupname)?; + #[cfg(target_os = "linux")] + pub fn get_group_info(groupname: &str) -> Result, Box> { + let info = get_conf("/etc/group", groupname)?; - match info { - Some(parts) => { - let gid = parts.get(2).ok_or("Invalid group format")?.parse::()?; + match info { + Some(parts) => { + let gid = parts.get(2).ok_or("Invalid group format")?.parse::()?; - Ok(Some(gid)) + Ok(Some(gid)) + } + None => Ok(None), } - None => Ok(None), } -} -#[cfg(target_os = "linux")] -fn set_ids(uid: u32, gid: u32, supp_gids: &Vec) -> UResult<()> { - setgroups(supp_gids.as_slice()) - .map_err(|e| USimpleError::new(1, format!("Failed to set supp Gid: {}", e)))?; - setgid(Gid::from_raw(gid)) - .map_err(|e| USimpleError::new(1, format!("Failed to set Gid to {}: {}", gid, e)))?; - setuid(Uid::from_raw(uid)) - .map_err(|e| USimpleError::new(1, format!("Failed to set Uid to {}: {}", uid, e)))?; - - Ok(()) -} + #[cfg(target_os = "linux")] + pub fn set_ids(uid: u32, gid: u32, supp_gids: &Vec) -> UResult<()> { + setgroups(supp_gids.as_slice()) + .map_err(|e| USimpleError::new(1, format!("Failed to set supp Gid: {}", e)))?; + setgid(Gid::from_raw(gid)) + .map_err(|e| USimpleError::new(1, format!("Failed to set Gid to {}: {}", gid, e)))?; + setuid(Uid::from_raw(uid)) + .map_err(|e| USimpleError::new(1, format!("Failed to set Uid to {}: {}", uid, e)))?; -fn prepare_env( - cmd: &mut RunCommand, - home_dir: &str, - shell_path: &str, - username: &str, - preserve_env: bool, - whitelist_env: &Vec, - is_login: bool, -) { - const ROOT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; - - if is_login { - if preserve_env { - println!("--preserve-environment is ignored in case `--login` is enabled"); - } - cmd.env_clear(); + Ok(()) } - if is_login || !preserve_env { - match username { - "root" => { - cmd.env("PATH", ROOT_PATH); + + pub fn prepare_env( + cmd: &mut RunCommand, + home_dir: &str, + shell_path: &str, + username: &str, + preserve_env: bool, + whitelist_env: &Vec, + is_login: bool, + ) { + const ROOT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"; + + if is_login { + if preserve_env { + println!("--preserve-environment is ignored in case `--login` is enabled"); } - _ => { - cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin"); + cmd.env_clear(); + } + if is_login || !preserve_env { + match username { + "root" => { + cmd.env("PATH", ROOT_PATH); + } + _ => { + cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin"); + } } + cmd.env("HOME", home_dir); + cmd.env("USER", username); + cmd.env("LOGNAME", username); + cmd.env("SHELL", shell_path); } - cmd.env("HOME", home_dir); - cmd.env("USER", username); - cmd.env("LOGNAME", username); - cmd.env("SHELL", shell_path); - } - for item in whitelist_env { - if !["HOME", "USER", "LOGNAME", "SHELL", "PATH"].contains(&item.as_str()) { - if let Ok(val) = var(item) { - cmd.env(item, val); + for item in whitelist_env { + if !["HOME", "USER", "LOGNAME", "SHELL", "PATH"].contains(&item.as_str()) { + if let Ok(val) = var(item) { + cmd.env(item, val); + } } } } -} -#[cfg(target_os = "linux")] -fn run(cmd: &mut RunCommand) -> UResult { - let status = cmd - .spawn() - .map_err(|e| match e.kind() { - std::io::ErrorKind::NotFound => USimpleError::new(127, "command was not found"), - e => USimpleError::new(126, format!("Failed to spawn for process: {}", e)), - })? - .wait() - .map_err(|e| USimpleError::new(126, format!("Failed to wait for process: {}", e)))?; - - Ok(status.code().unwrap_or(0)) -} + #[cfg(target_os = "linux")] + pub fn run(cmd: &mut RunCommand) -> UResult { + let status = cmd + .spawn() + .map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => USimpleError::new(127, "command was not found"), + e => USimpleError::new(126, format!("Failed to spawn for process: {}", e)), + })? + .wait() + .map_err(|e| USimpleError::new(126, format!("Failed to wait for process: {}", e)))?; + + Ok(status.code().unwrap_or(0)) + } -#[cfg(target_os = "linux")] -fn sep_session() -> UResult<()> { - setsid().map_err(|e| USimpleError::new(1, format!("Failed to set Sid: {}", e)))?; + #[cfg(target_os = "linux")] + pub fn sep_session() -> UResult<()> { + setsid().map_err(|e| USimpleError::new(1, format!("Failed to set Sid: {}", e)))?; - Ok(()) + Ok(()) + } } +#[cfg(target_os = "linux")] +use linux::*; + #[cfg(target_os = "linux")] #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + use nix::unistd::Gid; + use std::process::Command as RunCommand; let matches = uu_app().try_get_matches_from(args)?; let mut is_login = false; let mut command_args = Vec::new(); @@ -302,7 +309,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn main(args: impl uucore::Args) -> UResult<()> { let _matches = uu_app().try_get_matches_from(args)?; - Err(USimpleError::new(1, "`runuser` is only available on linux")) + Err(uucore::error::USimpleError::new( + 1, + "`runuser` is only available on linux", + )) } pub fn uu_app() -> Command { From 53e49d2e5f6fd5d54dfca3530f30da096412410c Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 12:28:45 +0300 Subject: [PATCH 5/7] renamed main to uumain to fix compilation error --- src/uu/runuser/src/runuser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index 9ee4c2ba..dcfac9f5 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -306,7 +306,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // on non-linux unix operating systems. #[cfg(not(target_os = "linux"))] #[uucore::main] -pub fn main(args: impl uucore::Args) -> UResult<()> { +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let _matches = uu_app().try_get_matches_from(args)?; Err(uucore::error::USimpleError::new( From cc6abcef353c196c06f6339b56b6616add3732ba Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 12:43:17 +0300 Subject: [PATCH 6/7] runuser: fixed import warning and added semicolon in test --- src/uu/runuser/src/runuser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index dcfac9f5..230048f1 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; -use uucore::error::{UResult, USimpleError}; +use uucore::error::UResult; use uucore::{format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("runuser.md"); From 45a5b66c604003c289a0c0eee094a9fce7ec4346 Mon Sep 17 00:00:00 2001 From: Mustafa Elrasheid Date: Wed, 29 Jul 2026 12:55:43 +0300 Subject: [PATCH 7/7] added missing file and added missing import --- src/uu/runuser/src/runuser.rs | 2 ++ tests/by-util/test_runuser.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/uu/runuser/src/runuser.rs b/src/uu/runuser/src/runuser.rs index 230048f1..27ba8036 100644 --- a/src/uu/runuser/src/runuser.rs +++ b/src/uu/runuser/src/runuser.rs @@ -159,6 +159,8 @@ mod linux { #[cfg(target_os = "linux")] use linux::*; +#[cfg(target_os = "linux")] +use uucore::error::USimpleError; #[cfg(target_os = "linux")] #[uucore::main] diff --git a/tests/by-util/test_runuser.rs b/tests/by-util/test_runuser.rs index 273c1ecd..82959c15 100644 --- a/tests/by-util/test_runuser.rs +++ b/tests/by-util/test_runuser.rs @@ -61,5 +61,5 @@ fn unsupported_platform() { new_ucmd!() .fails() .code_is(1) - .stderr_contains("`runuser` is only available on linux") + .stderr_contains("`runuser` is only available on linux"); }