diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 7ad2f0908..e967d4137 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/chroot.rs" [dependencies] -getopts = "0.2.18" +clap= "2.33" uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["entries"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index ab654abf8..2c3bcbca4 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -10,60 +10,81 @@ #[macro_use] extern crate uucore; -use uucore::entries; -use uucore::libc::{self, chroot, setgid, setgroups, setuid}; - +use clap::{App, Arg}; use std::ffi::CString; use std::io::Error; use std::path::Path; use std::process::Command; +use uucore::entries; +use uucore::libc::{self, chroot, setgid, setgroups, setuid}; +static VERSION: &str = env!("CARGO_PKG_VERSION"); static NAME: &str = "chroot"; +static ABOUT: &str = "Run COMMAND with root directory set to NEWROOT."; static SYNTAX: &str = "[OPTION]... NEWROOT [COMMAND [ARG]...]"; -static SUMMARY: &str = "Run COMMAND with root directory set to NEWROOT."; -static LONG_HELP: &str = " - If COMMAND is not specified, it defaults to '$(SHELL) -i'. - If $(SHELL) is not set, /bin/sh is used. -"; + +mod options { + pub const NEWROOT: &str = "newroot"; + pub const USER: &str = "user"; + pub const GROUP: &str = "group"; + pub const GROUPS: &str = "groups"; + pub const USERSPEC: &str = "userspec"; +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optopt( - "u", - "user", - "User (ID or name) to switch before running the program", - "USER", + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(SYNTAX) + .arg(Arg::with_name(options::NEWROOT).hidden(true).required(true)) + .arg( + Arg::with_name(options::USER) + .short("u") + .long(options::USER) + .help("User (ID or name) to switch before running the program") + .value_name("USER"), ) - .optopt("g", "group", "Group (ID or name) to switch to", "GROUP") - .optopt( - "G", - "groups", - "Comma-separated list of groups to switch to", - "GROUP1,GROUP2...", + .arg( + Arg::with_name(options::GROUP) + .short("g") + .long(options::GROUP) + .help("Group (ID or name) to switch to") + .value_name("GROUP"), ) - .optopt( - "", - "userspec", - "Colon-separated user and group to switch to. \ + .arg( + Arg::with_name(options::GROUPS) + .short("G") + .long(options::GROUPS) + .help("Comma-separated list of groups to switch to") + .value_name("GROUP1,GROUP2..."), + ) + .arg( + Arg::with_name(options::USERSPEC) + .long(options::USERSPEC) + .help( + "Colon-separated user and group to switch to. \ Same as -u USER -g GROUP. \ Userspec has higher preference than -u and/or -g", - "USER:GROUP", + ) + .value_name("USER:GROUP"), ) - .parse(args); - - if matches.free.is_empty() { - println!("Missing operand: NEWROOT"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } + .get_matches_from(args); let default_shell: &'static str = "/bin/sh"; let default_option: &'static str = "-i"; let user_shell = std::env::var("SHELL"); - let newroot = Path::new(&matches.free[0][..]); + let newroot: &Path = match matches.value_of(options::NEWROOT) { + Some(v) => Path::new(v), + None => crash!( + 1, + "Missing operand: NEWROOT\nTry '{} --help' for more information.", + NAME + ), + }; + if !newroot.is_dir() { crash!( 1, @@ -72,7 +93,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ); } - let command: Vec<&str> = match matches.free.len() { + let command: Vec<&str> = match matches.args.len() { 1 => { let shell: &str = match user_shell { Err(_) => default_shell, @@ -80,7 +101,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; vec![shell, default_option] } - _ => matches.free[1..].iter().map(|x| &x[..]).collect(), + _ => { + let mut vector: Vec<&str> = Vec::new(); + for (&k, v) in matches.args.iter() { + vector.push(k.clone()); + vector.push(&v.vals[0].to_str().unwrap()); + } + vector + } }; set_context(&newroot, &matches); @@ -97,30 +125,26 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } -fn set_context(root: &Path, options: &getopts::Matches) { - let userspec_str = options.opt_str("userspec"); - let user_str = options.opt_str("user").unwrap_or_default(); - let group_str = options.opt_str("group").unwrap_or_default(); - let groups_str = options.opt_str("groups").unwrap_or_default(); +fn set_context(root: &Path, options: &clap::ArgMatches) { + let userspec_str = options.value_of(options::USERSPEC); + let user_str = options.value_of(options::USER).unwrap_or_default(); + let group_str = options.value_of(options::GROUP).unwrap_or_default(); + let groups_str = options.value_of(options::GROUPS).unwrap_or_default(); let userspec = match userspec_str { Some(ref u) => { let s: Vec<&str> = u.split(':').collect(); - if s.len() != 2 { + if s.len() != 2 || s.iter().any(|&spec| spec == "") { crash!(1, "invalid userspec: `{}`", u) }; s } None => Vec::new(), }; - let user = if userspec.is_empty() { - &user_str[..] + + let (user, group) = if userspec.is_empty() { + (&user_str[..], &group_str[..]) } else { - &userspec[0][..] - }; - let group = if userspec.is_empty() { - &group_str[..] - } else { - &userspec[1][..] + (&userspec[0][..], &userspec[1][..]) }; enter_chroot(root); diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 651491045..9a8fb71dd 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -1 +1,98 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_missing_operand() { + let result = new_ucmd!().run(); + + assert_eq!( + true, + result + .stderr + .starts_with("error: The following required arguments were not provided") + ); + + assert_eq!(true, result.stderr.contains("")); +} + +#[test] +fn test_enter_chroot_fails() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("jail"); + + let result = ucmd.arg("jail").run(); + + assert_eq!( + true, + result.stderr.starts_with( + "chroot: error: cannot chroot to jail: Operation not permitted (os error 1)" + ) + ) +} + +#[test] +fn test_no_such_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch(&at.plus_as_string("a")); + + ucmd.arg("a") + .fails() + .stderr_is("chroot: error: cannot change root directory to `a`: no such directory"); +} + +#[test] +fn test_invalid_user_spec() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + let result = ucmd.arg("a").arg("--userspec=ARABA:").run(); + + assert_eq!( + true, + result.stderr.starts_with("chroot: error: invalid userspec") + ); +} + +#[test] +fn test_preference_of_userspec() { + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let username = result.stdout.trim_end(); + + let ts = TestScenario::new("id"); + let result = ts.cmd("id").arg("-g").arg("-n").run(); + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + + if is_ci() && result.stderr.contains("cannot find name for user ID") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + + let group_name = result.stdout.trim_end(); + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + let result = ucmd + .arg("a") + .arg("--user") + .arg("fake") + .arg("-G") + .arg("ABC,DEF") + .arg(format!("--userspec={}:{}", username, group_name)) + .run(); + + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); +}