diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a8ed1b7046e..fcaddd31089 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 504328488f2..a059c1cd55f 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 4f8f92fe45d..9037745eba9 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}; - - 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; - } +static ABOUT: &str = "Print user and group information for each specified USER, +or (when USER omitted) for the current user."; + +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."; +fn get_usage() -> String { + format!("{0} [OPTION]... [USER]...", executable!()) +} -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"; +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.", + ) +} -static ARG_USERS: &str = "users"; +struct Ids { + uid: u32, // user id + gid: u32, // group id + euid: u32, // effective uid + egid: u32, // effective gid +} -fn get_usage() -> String { - format!("{0} [OPTION]... [USER]", executable!()) +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)"), + .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 process audit user ID and other process audit properties,\n\ + which requires privilege (not available on Linux).", + ), ) .arg( - Arg::with_name(OPT_EFFECTIVE_USER) + Arg::with_name(options::OPT_EFFECTIVE_USER) .short("u") - .long("user") - .help("Display the effective user ID as a number"), + .long(options::OPT_EFFECTIVE_USER) + .conflicts_with(options::OPT_GROUP) + .help("Display only the effective user ID as a number."), ) .arg( - Arg::with_name(OPT_GROUP) + Arg::with_name(options::OPT_GROUP) .short("g") - .long(OPT_GROUP) - .help("Display the effective group ID as a number"), + .long(options::OPT_GROUP) + .help("Display only the effective group ID as a number"), ) .arg( - Arg::with_name(OPT_GROUPS) + Arg::with_name(options::OPT_GROUPS) .short("G") - .long(OPT_GROUPS) - .help("Display the different group IDs"), + .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(OPT_HUMAN_READABLE) + Arg::with_name(options::OPT_HUMAN_READABLE) .short("p") - .help("Make the output human-readable"), + .help("Make the output human-readable. Each display is on a separate line."), ) .arg( - Arg::with_name(OPT_NAME) + Arg::with_name(options::OPT_NAME) .short("n") - .help("Display the name of the user or group ID for the -G, -g and -u options"), + .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(OPT_PASSWORD) + Arg::with_name(options::OPT_PASSWORD) .short("P") - .help("Display the id as a password file entry"), + .help("Display the id as a password file entry."), ) .arg( - Arg::with_name(OPT_REAL_ID) + Arg::with_name(options::OPT_REAL_ID) .short("r") - .long(OPT_REAL_ID) + .long(options::OPT_REAL_ID) .help( - "Display the real ID for the -G, -g and -u options instead of the effective ID.", - ), + "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 line_ending = { + if state.zflag { + '\0' + } else { + '\n' + } + }; + let mut exit_code = 0; - 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() + 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; + } + } } - ); - return 0; - } + }; + + // 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; + } - 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; - } + 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 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()) + 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 { - id.to_string() - }) - .collect::>() - .join(" ") - ); - return 0; - } + uid.to_string() + } + ); + } - if matches.is_present(OPT_PASSWORD) { - pline(possible_pw.map(|v| v.uid())); - return 0; - }; + 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 matches.is_present(OPT_HUMAN_READABLE) { - pretty(possible_pw); - return 0; - } + if default_format { + id_print(&state, groups); + } + print!("{}", line_ending); - if possible_pw.is_some() { - id_print(possible_pw, false, false) - } else { - id_print(possible_pw, true, true) + if i + 1 >= users.len() { + break; + } } - 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/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index bc416634604..6b986e61638 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -47,6 +47,9 @@ use std::io::Result as IOResult; use std::ptr; extern "C" { + /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html + /// > The getgrouplist() function scans the group database to obtain + /// > the list of groups that user belongs to. fn getgrouplist( name: *const c_char, gid: gid_t, @@ -55,6 +58,13 @@ extern "C" { ) -> c_int; } +/// From: https://man7.org/linux/man-pages/man2/getgroups.2.html +/// > getgroups() returns the supplementary group IDs of the calling +/// > process in list. +/// > If size is zero, list is not modified, but the total number of +/// > supplementary group IDs for the process is returned. This allows +/// > the caller to determine the size of a dynamically allocated list +/// > to be used in a further call to getgroups(). pub fn get_groups() -> IOResult> { let ngroups = unsafe { getgroups(0, ptr::null_mut()) }; if ngroups == -1 { @@ -83,17 +93,17 @@ pub fn get_groups() -> IOResult> { /// for `id --groups --real` if `gid` and `egid` are not equal. /// /// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html -/// As implied by the definition of supplementary groups, the -/// effective group ID may appear in the array returned by -/// getgroups() or it may be returned only by getegid(). Duplication -/// may exist, but the application needs to call getegid() to be sure -/// of getting all of the information. Various implementation -/// variations and administrative sequences cause the set of groups -/// appearing in the result of getgroups() to vary in order and as to -/// whether the effective group ID is included, even when the set of -/// groups is the same (in the mathematical sense of ``set''). (The -/// history of a process and its parents could affect the details of -/// the result.) +/// > As implied by the definition of supplementary groups, the +/// > effective group ID may appear in the array returned by +/// > getgroups() or it may be returned only by getegid(). Duplication +/// > may exist, but the application needs to call getegid() to be sure +/// > of getting all of the information. Various implementation +/// > variations and administrative sequences cause the set of groups +/// > appearing in the result of getgroups() to vary in order and as to +/// > whether the effective group ID is included, even when the set of +/// > groups is the same (in the mathematical sense of ``set''). (The +/// > history of a process and its parents could affect the details of +/// > the result.) #[cfg(all(unix, feature = "process"))] pub fn get_groups_gnu(arg_id: Option) -> IOResult> { let groups = get_groups()?; @@ -184,16 +194,38 @@ impl Passwd { self.inner } + /// This is a wrapper function for `libc::getgrouplist`. + /// + /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html + /// > If the number of groups of which user is a member is less than or + /// > equal to *ngroups, then the value *ngroups is returned. + /// > If the user is a member of more than *ngroups groups, then + /// > getgrouplist() returns -1. In this case, the value returned in + /// > *ngroups can be used to resize the buffer passed to a further + /// > call getgrouplist(). + /// + /// However, on macOS/darwin (and maybe others?) `getgrouplist` does + /// not update `ngroups` if `ngroups` is too small. Therefore, if not + /// updated by `getgrouplist`, `ngroups` needs to be increased in a + /// loop until `getgrouplist` stops returning -1. pub fn belongs_to(&self) -> Vec { let mut ngroups: c_int = 8; + let mut ngroups_old: c_int; let mut groups = Vec::with_capacity(ngroups as usize); let gid = self.inner.pw_gid; let name = self.inner.pw_name; - unsafe { - if getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) == -1 { + loop { + ngroups_old = ngroups; + if unsafe { getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) } == -1 { + if ngroups == ngroups_old { + ngroups *= 2; + } groups.resize(ngroups as usize, 0); - getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups); + } else { + break; } + } + unsafe { groups.set_len(ngroups as usize); } groups.truncate(ngroups as usize); diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index cda41bb4fbe..21bfa992c02 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -17,18 +17,22 @@ use std::process::ExitStatus as StdExitStatus; use std::thread; use std::time::{Duration, Instant}; +/// `geteuid()` returns the effective user ID of the calling process. pub fn geteuid() -> uid_t { unsafe { libc::geteuid() } } +/// `getegid()` returns the effective group ID of the calling process. pub fn getegid() -> gid_t { unsafe { libc::getegid() } } +/// `getgid()` returns the real group ID of the calling process. pub fn getgid() -> gid_t { unsafe { libc::getgid() } } +/// `getuid()` returns the real user ID of the calling process. pub fn getuid() -> uid_t { unsafe { libc::getuid() } } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index c3a08810a1f..b4b929a2c27 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(); - } - } - false -} +// spell-checker:ignore (ToDO) testsuite coreutil -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(""); - } +// 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. - result.stdout_str().trim().to_string() -} - -#[test] -fn test_id() { - let scene = TestScenario::new(util_name!()); +// 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"; - let result = scene.ucmd().arg("-u").succeeds(); - let uid = result.stdout_str().trim(); +macro_rules! unwrap_or_return { + ( $e:expr ) => { + match $e { + Ok(x) => x, + Err(e) => { + println!("{}: test skipped: {}", UUTILS_INFO, e); + return; + } + } + }; +} - let result = scene.ucmd().run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; - } +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" - // Verify that the id found by --user/-u exists in the list - result.stdout_contains(uid); + // 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_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; - } +#[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 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 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 username_whoami = return_whoami_username(); - if username_whoami.is_empty() { - return; + // 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()); } - - assert_eq!(username_id, username_whoami); } #[test] -fn test_id_group() { - let scene = TestScenario::new(util_name!()); - - let mut result = scene.ucmd().arg("-g").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); +#[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)); - result = scene.ucmd().arg("--group").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + // 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(any(target_vendor = "apple", target_os = "linux"))] -fn test_id_groups() { +#[cfg(unix)] +fn test_id_name() { let scene = TestScenario::new(util_name!()); - for g_flag in &["-G", "--groups"] { - 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)); + 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() { + // `-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)); +} + +#[test] +#[cfg(unix)] +fn test_id_multiple_users() { + #[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 result = new_ucmd!().arg("-P").succeeds(); + // Same typical users that GNU testsuite is using. + let test_users = ["root", "man", "postfix", "sshd", &whoami()]; - assert!(result.stdout_str().starts_with(&username)); + 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()); + } } -#[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_non_existing() { #[cfg(target_os = "linux")] let util_name = util_name!(); - #[cfg(target_vendor = "apple")] - let util_name = format!("g{}", util_name!()); - - let result = if !exp_fail { - TestScenario::new(&util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) + #[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_move_str() - } else { - TestScenario::new(&util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) + .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_move_str() - }; - return if cfg!(target_os = "macos") && result.starts_with("gid") { - result[1..].to_string() + .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 { - result + // 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(), + )) }