diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a8ed1b704..fcaddd310 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -235,6 +235,7 @@ jobs: # { os, target, cargo-options, features, use-cross, toolchain } - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross } - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - { os: ubuntu-16.04 , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only diff --git a/Cargo.lock b/Cargo.lock index 504328488..a059c1cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - [[package]] name = "Inflector" version = "0.11.4" diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 4f8f92fe4..9037745eb 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -6,11 +6,27 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// Synced with: +// This was originally based on BSD's `id` +// (noticeable in functionality, usage text, options text, etc.) +// and synced with: // http://ftp-archive.freebsd.org/mirror/FreeBSD-Archive/old-releases/i386/1.0-RELEASE/ports/shellutils/src/id.c // http://www.opensource.apple.com/source/shell_cmds/shell_cmds-118/id/id.c +// +// * This was partially rewritten in order for stdout/stderr/exit_code +// to be conform with GNU coreutils (8.32) testsuite for `id`. +// +// * This supports multiple users (a feature that was introduced in coreutils 8.31) +// +// * This passes GNU's coreutils Testsuite (8.32) +// for "tests/id/uid.sh" and "tests/id/zero/sh". +// +// * Option '--zero' does not exist for BSD's `id`, therefore '--zero' is only +// allowed together with other options that are available on GNU's `id`. +// +// * Help text based on BSD's `id` manpage and GNU's `id` manpage. +// -// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag +// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag testsuite #![allow(non_camel_case_types)] #![allow(dead_code)] @@ -31,211 +47,342 @@ macro_rules! cstr2cow { }; } -#[cfg(not(target_os = "linux"))] -mod audit { - use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t}; +static ABOUT: &str = "Print user and group information for each specified USER, +or (when USER omitted) for the current user."; - pub type au_id_t = uid_t; - pub type au_asid_t = pid_t; - pub type au_event_t = c_uint; - pub type au_emod_t = c_uint; - pub type au_class_t = c_int; - pub type au_flag_t = u64; - - #[repr(C)] - pub struct au_mask { - pub am_success: c_uint, - pub am_failure: c_uint, - } - pub type au_mask_t = au_mask; - - #[repr(C)] - pub struct au_tid_addr { - pub port: dev_t, - } - pub type au_tid_addr_t = au_tid_addr; - - #[repr(C)] - pub struct c_auditinfo_addr { - pub ai_auid: au_id_t, // Audit user ID - pub ai_mask: au_mask_t, // Audit masks. - pub ai_termid: au_tid_addr_t, // Terminal ID. - pub ai_asid: au_asid_t, // Audit session ID. - pub ai_flags: au_flag_t, // Audit session flags - } - pub type c_auditinfo_addr_t = c_auditinfo_addr; - - extern "C" { - pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int; - } +mod options { + pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this + pub const OPT_CONTEXT: &str = "context"; + pub const OPT_EFFECTIVE_USER: &str = "user"; + pub const OPT_GROUP: &str = "group"; + pub const OPT_GROUPS: &str = "groups"; + pub const OPT_HUMAN_READABLE: &str = "human-readable"; // GNU's id does not have this + pub const OPT_NAME: &str = "name"; + pub const OPT_PASSWORD: &str = "password"; // GNU's id does not have this + pub const OPT_REAL_ID: &str = "real"; + pub const OPT_ZERO: &str = "zero"; // BSD's id does not have this + pub const ARG_USERS: &str = "USER"; } -static ABOUT: &str = "Display user and group information for the specified USER,\n or (when USER omitted) for the current user."; - -static OPT_AUDIT: &str = "audit"; -static OPT_EFFECTIVE_USER: &str = "effective-user"; -static OPT_GROUP: &str = "group"; -static OPT_GROUPS: &str = "groups"; -static OPT_HUMAN_READABLE: &str = "human-readable"; -static OPT_NAME: &str = "name"; -static OPT_PASSWORD: &str = "password"; -static OPT_REAL_ID: &str = "real"; - -static ARG_USERS: &str = "users"; - fn get_usage() -> String { - format!("{0} [OPTION]... [USER]", executable!()) + format!("{0} [OPTION]... [USER]...", executable!()) +} + +fn get_description() -> String { + String::from( + "The id utility displays the user and group names and numeric IDs, of the \ + calling process, to the standard output. If the real and effective IDs are \ + different, both are displayed, otherwise only the real ID is displayed.\n\n\ + If a user (login name or user ID) is specified, the user and group IDs of \ + that user are displayed. In this case, the real and effective IDs are \ + assumed to be the same.", + ) +} + +struct Ids { + uid: u32, // user id + gid: u32, // group id + euid: u32, // effective uid + egid: u32, // effective gid +} + +struct State { + nflag: bool, // --name + uflag: bool, // --user + gflag: bool, // --group + gsflag: bool, // --groups + rflag: bool, // --real + zflag: bool, // --zero + ids: Option, + // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different. + // * The SELinux context is only displayed without a specified user. + // * The `getgroups` system call is only used without a specified user, this causes + // the order of the displayed groups to be different between `id` and `id $USER`. + // + // Example: + // $ strace -e getgroups id -G $USER + // 1000 10 975 968 + // +++ exited with 0 +++ + // $ strace -e getgroups id -G + // getgroups(0, NULL) = 4 + // getgroups(4, [10, 968, 975, 1000]) = 4 + // 1000 10 968 975 + // +++ exited with 0 +++ + user_specified: bool, } pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); + let after_help = get_description(); let matches = App::new(executable!()) .version(crate_version!()) .about(ABOUT) .usage(&usage[..]) + .after_help(&after_help[..]) .arg( - Arg::with_name(OPT_AUDIT) + Arg::with_name(options::OPT_AUDIT) .short("A") - .help("Display the process audit (not available on Linux)"), - ) - .arg( - Arg::with_name(OPT_EFFECTIVE_USER) - .short("u") - .long("user") - .help("Display the effective user ID as a number"), - ) - .arg( - Arg::with_name(OPT_GROUP) - .short("g") - .long(OPT_GROUP) - .help("Display the effective group ID as a number"), - ) - .arg( - Arg::with_name(OPT_GROUPS) - .short("G") - .long(OPT_GROUPS) - .help("Display the different group IDs"), - ) - .arg( - Arg::with_name(OPT_HUMAN_READABLE) - .short("p") - .help("Make the output human-readable"), - ) - .arg( - Arg::with_name(OPT_NAME) - .short("n") - .help("Display the name of the user or group ID for the -G, -g and -u options"), - ) - .arg( - Arg::with_name(OPT_PASSWORD) - .short("P") - .help("Display the id as a password file entry"), - ) - .arg( - Arg::with_name(OPT_REAL_ID) - .short("r") - .long(OPT_REAL_ID) + .conflicts_with_all(&[ + options::OPT_GROUP, + options::OPT_EFFECTIVE_USER, + options::OPT_HUMAN_READABLE, + options::OPT_PASSWORD, + options::OPT_GROUPS, + options::OPT_ZERO, + ]) .help( - "Display the real ID for the -G, -g and -u options instead of the effective ID.", - ), + "Display the process audit user ID and other process audit properties,\n\ + which requires privilege (not available on Linux).", + ), + ) + .arg( + Arg::with_name(options::OPT_EFFECTIVE_USER) + .short("u") + .long(options::OPT_EFFECTIVE_USER) + .conflicts_with(options::OPT_GROUP) + .help("Display only the effective user ID as a number."), + ) + .arg( + Arg::with_name(options::OPT_GROUP) + .short("g") + .long(options::OPT_GROUP) + .help("Display only the effective group ID as a number"), + ) + .arg( + Arg::with_name(options::OPT_GROUPS) + .short("G") + .long(options::OPT_GROUPS) + .conflicts_with_all(&[ + options::OPT_GROUP, + options::OPT_EFFECTIVE_USER, + options::OPT_HUMAN_READABLE, + options::OPT_PASSWORD, + options::OPT_AUDIT, + ]) + .help( + "Display only the different group IDs as white-space separated numbers, \ + in no particular order.", + ), + ) + .arg( + Arg::with_name(options::OPT_HUMAN_READABLE) + .short("p") + .help("Make the output human-readable. Each display is on a separate line."), + ) + .arg( + Arg::with_name(options::OPT_NAME) + .short("n") + .long(options::OPT_NAME) + .help( + "Display the name of the user or group ID for the -G, -g and -u options \ + instead of the number.\nIf any of the ID numbers cannot be mapped into \ + names, the number will be displayed as usual.", + ), + ) + .arg( + Arg::with_name(options::OPT_PASSWORD) + .short("P") + .help("Display the id as a password file entry."), + ) + .arg( + Arg::with_name(options::OPT_REAL_ID) + .short("r") + .long(options::OPT_REAL_ID) + .help( + "Display the real ID for the -G, -g and -u options instead of \ + the effective ID.", + ), + ) + .arg( + Arg::with_name(options::OPT_ZERO) + .short("z") + .long(options::OPT_ZERO) + .help( + "delimit entries with NUL characters, not whitespace;\n\ + not permitted in default format", + ), + ) + .arg( + Arg::with_name(options::OPT_CONTEXT) + .short("Z") + .long(options::OPT_CONTEXT) + .help("NotImplemented: print only the security context of the process"), + ) + .arg( + Arg::with_name(options::ARG_USERS) + .multiple(true) + .takes_value(true) + .value_name(options::ARG_USERS), ) - .arg(Arg::with_name(ARG_USERS).multiple(true).takes_value(true)) .get_matches_from(args); let users: Vec = matches - .values_of(ARG_USERS) + .values_of(options::ARG_USERS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - if matches.is_present(OPT_AUDIT) { - auditid(); - return 0; + let mut state = State { + nflag: matches.is_present(options::OPT_NAME), + uflag: matches.is_present(options::OPT_EFFECTIVE_USER), + gflag: matches.is_present(options::OPT_GROUP), + gsflag: matches.is_present(options::OPT_GROUPS), + rflag: matches.is_present(options::OPT_REAL_ID), + zflag: matches.is_present(options::OPT_ZERO), + user_specified: !users.is_empty(), + ids: None, + }; + + let default_format = { + // "default format" is when none of '-ugG' was used + !(state.uflag || state.gflag || state.gsflag) + }; + + if (state.nflag || state.rflag) && default_format { + crash!(1, "cannot print only names or real IDs in default format"); + } + if (state.zflag) && default_format { + // NOTE: GNU testsuite "id/zero.sh" needs this stderr output: + crash!(1, "option --zero not permitted in default format"); } - let possible_pw = if users.is_empty() { - None - } else { - match Passwd::locate(users[0].as_str()) { - Ok(p) => Some(p), - Err(_) => crash!(1, "No such user/group: {}", users[0]), + let delimiter = { + if state.zflag { + "\0".to_string() + } else { + " ".to_string() } }; - - let nflag = matches.is_present(OPT_NAME); - let uflag = matches.is_present(OPT_EFFECTIVE_USER); - let gflag = matches.is_present(OPT_GROUP); - let gsflag = matches.is_present(OPT_GROUPS); - let rflag = matches.is_present(OPT_REAL_ID); - - if gflag { - let id = possible_pw - .map(|p| p.gid()) - .unwrap_or(if rflag { getgid() } else { getegid() }); - println!( - "{}", - if nflag { - entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) - } else { - id.to_string() - } - ); - return 0; - } - - if uflag { - let id = possible_pw - .map(|p| p.uid()) - .unwrap_or(if rflag { getuid() } else { geteuid() }); - println!( - "{}", - if nflag { - entries::uid2usr(id).unwrap_or_else(|_| id.to_string()) - } else { - id.to_string() - } - ); - return 0; - } - - if gsflag { - let id = possible_pw - .map(|p| p.gid()) - .unwrap_or(if rflag { getgid() } else { getegid() }); - println!( - "{}", - possible_pw - .map(|p| p.belongs_to()) - .unwrap_or_else(|| entries::get_groups_gnu(Some(id)).unwrap()) - .iter() - .map(|&id| if nflag { - entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) - } else { - id.to_string() - }) - .collect::>() - .join(" ") - ); - return 0; - } - - if matches.is_present(OPT_PASSWORD) { - pline(possible_pw.map(|v| v.uid())); - return 0; + let line_ending = { + if state.zflag { + '\0' + } else { + '\n' + } }; + let mut exit_code = 0; - if matches.is_present(OPT_HUMAN_READABLE) { - pretty(possible_pw); - return 0; + for i in 0..=users.len() { + let possible_pw = if !state.user_specified { + None + } else { + match Passwd::locate(users[i].as_str()) { + Ok(p) => Some(p), + Err(_) => { + show_error!("‘{}’: no such user", users[i]); + exit_code = 1; + if i + 1 >= users.len() { + break; + } else { + continue; + } + } + } + }; + + // GNU's `id` does not support the flags: -p/-P/-A. + if matches.is_present(options::OPT_PASSWORD) { + // BSD's `id` ignores all but the first specified user + pline(possible_pw.map(|v| v.uid())); + return exit_code; + }; + if matches.is_present(options::OPT_HUMAN_READABLE) { + // BSD's `id` ignores all but the first specified user + pretty(possible_pw); + return exit_code; + } + if matches.is_present(options::OPT_AUDIT) { + // BSD's `id` ignores specified users + auditid(); + return exit_code; + } + + let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or(( + if state.rflag { getuid() } else { geteuid() }, + if state.rflag { getgid() } else { getegid() }, + )); + state.ids = Some(Ids { + uid, + gid, + euid: geteuid(), + egid: getegid(), + }); + + if state.gflag { + print!( + "{}", + if state.nflag { + entries::gid2grp(gid).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", gid); + exit_code = 1; + gid.to_string() + }) + } else { + gid.to_string() + } + ); + } + + if state.uflag { + print!( + "{}", + if state.nflag { + entries::uid2usr(uid).unwrap_or_else(|_| { + show_error!("cannot find name for user ID {}", uid); + exit_code = 1; + uid.to_string() + }) + } else { + uid.to_string() + } + ); + } + + let groups = entries::get_groups_gnu(Some(gid)).unwrap(); + let groups = if state.user_specified { + possible_pw.map(|p| p.belongs_to()).unwrap() + } else { + groups.clone() + }; + + if state.gsflag { + print!( + "{}{}", + groups + .iter() + .map(|&id| { + if state.nflag { + entries::gid2grp(id).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", id); + exit_code = 1; + id.to_string() + }) + } else { + id.to_string() + } + }) + .collect::>() + .join(&delimiter), + // NOTE: this is necessary to pass GNU's "tests/id/zero.sh": + if state.zflag && state.user_specified && users.len() > 1 { + "\0" + } else { + "" + } + ); + } + + if default_format { + id_print(&state, groups); + } + print!("{}", line_ending); + + if i + 1 >= users.len() { + break; + } } - if possible_pw.is_some() { - id_print(possible_pw, false, false) - } else { - id_print(possible_pw, true, true) - } - - 0 + exit_code } fn pretty(possible_pw: Option) { @@ -348,30 +495,21 @@ fn auditid() { println!("asid={}", auditinfo.ai_asid); } -fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { - let (uid, gid) = possible_pw - .map(|p| (p.uid(), p.gid())) - .unwrap_or((getuid(), getgid())); - - let groups = match Passwd::locate(uid) { - Ok(p) => p.belongs_to(), - Err(e) => crash!(1, "Could not find uid {}: {}", uid, e), - }; +fn id_print(state: &State, groups: Vec) { + let uid = state.ids.as_ref().unwrap().uid; + let gid = state.ids.as_ref().unwrap().gid; + let euid = state.ids.as_ref().unwrap().euid; + let egid = state.ids.as_ref().unwrap().egid; print!("uid={}({})", uid, entries::uid2usr(uid).unwrap()); print!(" gid={}({})", gid, entries::gid2grp(gid).unwrap()); - - let euid = geteuid(); - if p_euid && (euid != uid) { + if !state.user_specified && (euid != uid) { print!(" euid={}({})", euid, entries::uid2usr(euid).unwrap()); } - - let egid = getegid(); - if p_egid && (egid != gid) { + if !state.user_specified && (egid != gid) { print!(" egid={}({})", euid, entries::gid2grp(egid).unwrap()); } - - println!( + print!( " groups={}", groups .iter() @@ -379,4 +517,49 @@ fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { .collect::>() .join(",") ); + + // NOTE: (SELinux NotImplemented) placeholder: + // if !state.user_specified { + // // print SElinux context (does not depend on "-Z") + // print!(" context={}", get_selinux_contexts().join(":")); + // } +} + +#[cfg(not(target_os = "linux"))] +mod audit { + use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t}; + + pub type au_id_t = uid_t; + pub type au_asid_t = pid_t; + pub type au_event_t = c_uint; + pub type au_emod_t = c_uint; + pub type au_class_t = c_int; + pub type au_flag_t = u64; + + #[repr(C)] + pub struct au_mask { + pub am_success: c_uint, + pub am_failure: c_uint, + } + pub type au_mask_t = au_mask; + + #[repr(C)] + pub struct au_tid_addr { + pub port: dev_t, + } + pub type au_tid_addr_t = au_tid_addr; + + #[repr(C)] + pub struct c_auditinfo_addr { + pub ai_auid: au_id_t, // Audit user ID + pub ai_mask: au_mask_t, // Audit masks. + pub ai_termid: au_tid_addr_t, // Terminal ID. + pub ai_asid: au_asid_t, // Audit session ID. + pub ai_flags: au_flag_t, // Audit session flags + } + pub type c_auditinfo_addr_t = c_auditinfo_addr; + + extern "C" { + pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int; + } } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index c3a08810a..b4b929a2c 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,151 +1,186 @@ use crate::common::util::*; -// Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. -// If we are running inside the CI and "needle" is in "stderr" skipping this test is -// considered okay. If we are not inside the CI this calls assert!(result.success). -// -// From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" -// stderr: "whoami: cannot find name for user ID 1001" -// Maybe: "adduser --uid 1001 username" can put things right? -// stderr = id: Could not find uid 1001: No such id: 1001 -fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { - if !result.succeeded() { - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stderr_str().contains(needle) { - println!("test skipped:"); - return true; - } else { - result.success(); +// spell-checker:ignore (ToDO) testsuite coreutil + +// These tests run the GNU coreutils `(g)id` binary in `$PATH` in order to gather reference values. +// If the `(g)id` in `$PATH` doesn't include a coreutils version string, +// or the version is too low, the test is skipped. + +// The reference version is 8.32. Here 8.30 was chosen because right now there's no +// ubuntu image for github action available with a higher version than 8.30. +const VERSION_EXPECTED: &str = "8.30"; // Version expected for the reference `id` in $PATH +const VERSION_MULTIPLE_USERS: &str = "8.31"; +const UUTILS_WARNING: &str = "uutils-tests-warning"; +const UUTILS_INFO: &str = "uutils-tests-info"; + +macro_rules! unwrap_or_return { + ( $e:expr ) => { + match $e { + Ok(x) => x, + Err(e) => { + println!("{}: test skipped: {}", UUTILS_INFO, e); + return; + } } - } - false + }; } -fn return_whoami_username() -> String { - let scene = TestScenario::new("whoami"); - let result = scene.cmd("whoami").run(); - if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { - println!("test skipped:"); - return String::from(""); - } +fn whoami() -> String { + // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. + // + // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" + // whoami: cannot find name for user ID 1001 + // id --name: cannot find name for user ID 1001 + // id --name: cannot find name for group ID 116 + // + // However, when running "id" from within "/bin/bash" it looks fine: + // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" + // whoami: "runner" - result.stdout_str().trim().to_string() + // Use environment variable to get current user instead of + // invoking `whoami` and fall back to user "nobody" on error. + std::env::var("USER").unwrap_or_else(|e| { + println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); + "nobody".to_string() + }) } #[test] -fn test_id() { - let scene = TestScenario::new(util_name!()); +#[cfg(unix)] +fn test_id_no_specified_user() { + let result = new_ucmd!().run(); + let exp_result = unwrap_or_return!(expected_result(&[])); + let mut _exp_stdout = exp_result.stdout_str().to_string(); - let result = scene.ucmd().arg("-u").succeeds(); - let uid = result.stdout_str().trim(); - - let result = scene.ucmd().run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; - } - - // Verify that the id found by --user/-u exists in the list - result.stdout_contains(uid); -} - -#[test] -fn test_id_from_name() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } - - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg(&username).run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; - } - - let uid = result.stdout_str().trim(); - - let result = scene.ucmd().run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; + #[cfg(target_os = "linux")] + { + // NOTE: (SELinux NotImplemented) strip 'context' part from exp_stdout: + if let Some(context_offset) = exp_result.stdout_str().find(" context=") { + _exp_stdout.replace_range(context_offset.._exp_stdout.len() - 1, ""); + } } result - // Verify that the id found by --user/-u exists in the list - .stdout_contains(uid) - // Verify that the username found by whoami exists in the list - .stdout_contains(username); + .stdout_is(_exp_stdout) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } #[test] -fn test_id_name_from_id() { - let result = new_ucmd!().arg("-nu").run(); +#[cfg(unix)] +fn test_id_single_user() { + let test_users = [&whoami()[..]]; - let username_id = result.stdout_str().trim(); - - let username_whoami = return_whoami_username(); - if username_whoami.is_empty() { - return; - } - - assert_eq!(username_id, username_whoami); -} - -#[test] -fn test_id_group() { let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); - let mut result = scene.ucmd().arg("-g").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); - - result = scene.ucmd().arg("--group").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); -} - -#[test] -#[cfg(any(target_vendor = "apple", target_os = "linux"))] -fn test_id_groups() { - let scene = TestScenario::new(util_name!()); - for g_flag in &["-G", "--groups"] { + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); scene .ucmd() - .arg(g_flag) - .succeeds() - .stdout_is(expected_result(&[g_flag], false)); - for &r_flag in &["-r", "--real"] { - let args = [g_flag, r_flag]; - scene - .ucmd() - .args(&args) - .succeeds() - .stdout_is(expected_result(&args, false)); + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_single_user_non_existing() { + let args = &["hopefully_non_existing_username"]; + let result = new_ucmd!().args(args).run(); + let exp_result = unwrap_or_return!(expected_result(args)); + + // It is unknown why on macOS (and possibly others?) `id` adds "Invalid argument". + // coreutils 8.32: $ LC_ALL=C id foobar + // macOS: stderr: "id: 'foobar': no such user: Invalid argument" + // linux: stderr: "id: 'foobar': no such user" + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); +} + +#[test] +#[cfg(unix)] +fn test_id_name() { + let scene = TestScenario::new(util_name!()); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--name"]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + + if opt == "--user" { + assert_eq!(result.stdout_str().trim_end(), whoami()); } } } #[test] -fn test_id_user() { +#[cfg(unix)] +fn test_id_real() { let scene = TestScenario::new(util_name!()); - - let result = scene.ucmd().arg("-u").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); - - let result = scene.ucmd().arg("--user").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--real"]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } } #[test] +#[cfg(all(unix, not(target_os = "linux")))] fn test_id_pretty_print() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } + // `-p` is BSD only and not supported on GNU's `id` + let username = whoami(); - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-p").run(); + let result = new_ucmd!().arg("-p").run(); if result.stdout_str().trim().is_empty() { // this fails only on: "MinRustV (ubuntu-latest, feat_os_unix)" // `rustc 1.40.0 (73528e339 2019-12-16)` @@ -154,49 +189,317 @@ fn test_id_pretty_print() { // stdout = // stderr = ', tests/common/util.rs:157:13 println!("test skipped:"); - return; + } else { + result.success().stdout_contains(username); } - - result.success().stdout_contains(username); } #[test] +#[cfg(all(unix, not(target_os = "linux")))] fn test_id_password_style() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } - - let result = new_ucmd!().arg("-P").succeeds(); - + // `-P` is BSD only and not supported on GNU's `id` + let username = whoami(); + let result = new_ucmd!().arg("-P").arg(&username).succeeds(); assert!(result.stdout_str().starts_with(&username)); } -#[cfg(any(target_vendor = "apple", target_os = "linux"))] -fn expected_result(args: &[&str], exp_fail: bool) -> String { +#[test] +#[cfg(unix)] +fn test_id_multiple_users() { #[cfg(target_os = "linux")] let util_name = util_name!(); - #[cfg(target_vendor = "apple")] - let util_name = format!("g{}", util_name!()); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + let version_check_string = check_coreutil_version(util_name, VERSION_MULTIPLE_USERS); + if version_check_string.starts_with(UUTILS_WARNING) { + println!("{}\ntest skipped", version_check_string); + return; + } - let result = if !exp_fail { - TestScenario::new(&util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) - .succeeds() - .stdout_move_str() - } else { - TestScenario::new(&util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) - .fails() - .stderr_move_str() - }; - return if cfg!(target_os = "macos") && result.starts_with("gid") { - result[1..].to_string() - } else { - result - }; + // Same typical users that GNU testsuite is using. + let test_users = ["root", "man", "postfix", "sshd", &whoami()]; + + let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_multiple_users_non_existing() { + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + let version_check_string = check_coreutil_version(util_name, VERSION_MULTIPLE_USERS); + if version_check_string.starts_with(UUTILS_WARNING) { + println!("{}\ntest skipped", version_check_string); + return; + } + + let test_users = [ + "root", + "hopefully_non_existing_username1", + &whoami(), + "man", + "hopefully_non_existing_username2", + "hopefully_non_existing_username3", + "postfix", + "sshd", + "hopefully_non_existing_username4", + &whoami(), + ]; + + let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_default_format() { + let scene = TestScenario::new(util_name!()); + for &opt1 in &["--name", "--real"] { + // id: cannot print only names or real IDs in default format + let args = [opt1]; + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&args)).stderr_str()); + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G n/r + let args = [opt2, opt1]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } + } + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G + let args = [opt2]; + scene + .ucmd() + .args(&args) + .succeeds() + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_zero() { + let scene = TestScenario::new(util_name!()); + for z_flag in &["-z", "--zero"] { + // id: option --zero not permitted in default format + scene + .ucmd() + .args(&[z_flag]) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&[z_flag])).stderr_str()); + for &opt1 in &["--name", "--real"] { + // id: cannot print only names or real IDs in default format + let args = [opt1, z_flag]; + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&args)).stderr_str()); + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G n/r z + let args = [opt2, z_flag, opt1]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } + } + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G z + let args = [opt2, z_flag]; + scene + .ucmd() + .args(&args) + .succeeds() + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); + } + } +} + +fn check_coreutil_version(util_name: &str, version_expected: &str) -> String { + // example: + // $ id --version | head -n 1 + // id (GNU coreutils) 8.32.162-4eda + let scene = TestScenario::new(util_name); + let version_check = scene + .cmd_keepenv(&util_name) + .env("LANGUAGE", "C") + .arg("--version") + .run(); + version_check + .stdout_str() + .split('\n') + .collect::>() + .get(0) + .map_or_else( + || format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name), + |s| { + if s.contains(&format!("(GNU coreutils) {}", version_expected)) { + s.to_string() + } else if s.contains("(GNU coreutils)") { + let version_found = s.split_whitespace().last().unwrap()[..4].parse::().unwrap_or_default(); + let version_expected = version_expected.parse::().unwrap_or_default(); + if version_found > version_expected { + format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found) + } else { + format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found) } + } else { + format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name) + } + }, + ) +} + +#[allow(clippy::needless_borrow)] +#[cfg(unix)] +fn expected_result(args: &[&str]) -> Result { + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + + let version_check_string = check_coreutil_version(util_name, VERSION_EXPECTED); + if version_check_string.starts_with(UUTILS_WARNING) { + return Err(version_check_string); + } + println!("{}", version_check_string); + + let scene = TestScenario::new(util_name); + let result = scene + .cmd_keepenv(util_name) + .env("LANGUAGE", "C") + .args(args) + .run(); + + let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { + ( + result.stdout_str().to_string(), + result.stderr_str().to_string(), + ) + } else { + // strip 'g' prefix from results: + let from = util_name.to_string() + ":"; + let to = &from[1..]; + ( + result.stdout_str().replace(&from, to), + result.stderr_str().replace(&from, to), + ) + }; + + Ok(CmdResult::new( + Some(result.tmpd()), + Some(result.code()), + result.succeeded(), + stdout.as_bytes(), + stderr.as_bytes(), + )) }