From 74a7da7b527a8942c45efd7b40c7be0f032bc85d Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sat, 5 Jun 2021 19:56:20 +0200 Subject: [PATCH 01/12] id: clean-up of clap options and usage/help text * add conflicts_with for '-G' * add required flags for '-r' and '-n' * add usage/help texts from BSD's `id` --- src/uu/id/src/id.rs | 99 +++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 77b185f24..b32988ebc 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -9,6 +9,10 @@ // 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 is not based on coreutils (8.32) GNU's `id`. +// This is based on BSD's `id` (noticeable in functionality, usage text, options text, etc.) +// // spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag @@ -70,18 +74,19 @@ mod audit { } } -static ABOUT: &str = "Display user and group information for the specified USER,\n or (when USER omitted) for the current user."; +static ABOUT: &str = "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\nIf 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 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-id"; - -static ARG_USERS: &str = "users"; +mod options { + pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this + 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 ARG_USERS: &str = "USER"; +} fn get_usage() -> String { format!("{0} [OPTION]... [USER]", executable!()) @@ -95,57 +100,76 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .about(ABOUT) .usage(&usage[..]) .arg( - Arg::with_name(OPT_AUDIT) + Arg::with_name(options::OPT_AUDIT) .short("A") - .help("Display the process audit (not available on Linux)"), + .help("Display the process audit user ID and other process audit properties, 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) + .help("Display the effective user ID as a number."), ) .arg( - Arg::with_name(OPT_GROUP) + Arg::with_name(options::OPT_GROUP) .short("g") - .long(OPT_GROUP) + .long(options::OPT_GROUP) .help("Display 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 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. If 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") - .help("Display the real ID for the -g and -u options"), + .long(options::OPT_REAL_ID) + .help("Display the real ID for the -g and -u options instead of the effective ID."), + ) + .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 nflag = matches.is_present(options::OPT_NAME); + let uflag = matches.is_present(options::OPT_EFFECTIVE_USER); + let gflag = matches.is_present(options::OPT_GROUP); + let gsflag = matches.is_present(options::OPT_GROUPS); + let rflag = matches.is_present(options::OPT_REAL_ID); + + // -ugG + if (nflag || rflag) && !(uflag || gflag || gsflag) { + crash!(1, "cannot print only names or real IDs in default format"); + } + 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) { + if matches.is_present(options::OPT_AUDIT) { auditid(); return 0; } @@ -159,11 +183,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; - let nflag = matches.is_present(OPT_NAME); - let uflag = matches.is_present(OPT_EFFECTIVE_USER); - let gflag = matches.is_present(OPT_GROUP); - let rflag = matches.is_present(OPT_REAL_ID); - if gflag { let id = possible_pw .map(|p| p.gid()) @@ -194,7 +213,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 0; } - if matches.is_present(OPT_GROUPS) { + if gsflag { println!( "{}", if nflag { @@ -218,12 +237,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 0; } - if matches.is_present(OPT_PASSWORD) { + if matches.is_present(options::OPT_PASSWORD) { pline(possible_pw.map(|v| v.uid())); return 0; }; - if matches.is_present(OPT_HUMAN_READABLE) { + if matches.is_present(options::OPT_HUMAN_READABLE) { pretty(possible_pw); return 0; } From 98225105afc35feb703c4fe8ce6c41add551c582 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 6 Jun 2021 00:03:53 +0200 Subject: [PATCH 02/12] id: implement '--zero' flag * add tests for '--zero' flag * add a bunch of requires/conflicts rules for flags (incl. tests) --- src/uu/id/src/id.rs | 56 +++++++++++++----- tests/by-util/test_id.rs | 123 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 22 deletions(-) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index b32988ebc..f44d77c5f 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -13,8 +13,10 @@ // This is not based on coreutils (8.32) GNU's `id`. // This is based on BSD's `id` (noticeable in functionality, usage text, options text, etc.) // +// Option '--zero' does not exist for BSD's `id`, therefor '--zero' is only allowed together +// with other options that are available on GNU's `id`. -// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag +// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag #![allow(non_camel_case_types)] #![allow(dead_code)] @@ -85,6 +87,7 @@ mod options { 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"; } @@ -102,26 +105,28 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::OPT_AUDIT) .short("A") + .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, which requires privilege (not available on Linux)."), ) .arg( Arg::with_name(options::OPT_EFFECTIVE_USER) .short("u") .long(options::OPT_EFFECTIVE_USER) - .help("Display the effective user ID as a number."), + .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 the effective group ID as a number"), + .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 the different group IDs as white-space separated numbers, in no particular order."), + .help("Display only the different group IDs as white-space separated numbers, in no particular order."), ) .arg( Arg::with_name(options::OPT_HUMAN_READABLE) @@ -145,6 +150,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::OPT_REAL_ID) .help("Display the real ID for the -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;\nnot permitted in default format"), + ) .arg( Arg::with_name(options::ARG_USERS) .multiple(true) @@ -158,11 +169,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let gflag = matches.is_present(options::OPT_GROUP); let gsflag = matches.is_present(options::OPT_GROUPS); let rflag = matches.is_present(options::OPT_REAL_ID); + let zflag = matches.is_present(options::OPT_ZERO); - // -ugG + // "default format" is when none of '-ugG' was used + // could not implement these "required" rules with just clap if (nflag || rflag) && !(uflag || gflag || gsflag) { crash!(1, "cannot print only names or real IDs in default format"); } + if (zflag) && !(uflag || gflag || gsflag) { + crash!(1, "option --zero not permitted in default format"); + } let users: Vec = matches .values_of(options::ARG_USERS) @@ -183,17 +199,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; + let line_ending = if zflag { '\0' } else { '\n' }; + if gflag { let id = possible_pw .map(|p| p.gid()) .unwrap_or(if rflag { getgid() } else { getegid() }); - println!( - "{}", + print!( + "{}{}", if nflag { entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) } else { id.to_string() - } + }, + line_ending ); return 0; } @@ -202,20 +221,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let id = possible_pw .map(|p| p.uid()) .unwrap_or(if rflag { getuid() } else { geteuid() }); - println!( - "{}", + print!( + "{}{}", if nflag { entries::uid2usr(id).unwrap_or_else(|_| id.to_string()) } else { id.to_string() - } + }, + line_ending ); return 0; } if gsflag { - println!( - "{}", + let delimiter = if zflag { "" } else { " " }; + print!( + "{}{}", if nflag { possible_pw .map(|p| p.belongs_to()) @@ -223,7 +244,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .iter() .map(|&id| entries::gid2grp(id).unwrap()) .collect::>() - .join(" ") + .join(delimiter) } else { possible_pw .map(|p| p.belongs_to()) @@ -231,8 +252,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .iter() .map(|&id| id.to_string()) .collect::>() - .join(" ") - } + .join(delimiter) + }, + line_ending ); return 0; } @@ -398,3 +420,5 @@ fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { .join(",") ); } + +fn get_groups() -> diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 1f8249aab..a8ad37190 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -96,11 +96,19 @@ fn test_id_group() { let mut result = scene.ucmd().arg("-g").succeeds(); let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + assert!(s1.parse::().is_ok()); result = scene.ucmd().arg("--group").succeeds(); let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + assert!(s1.parse::().is_ok()); + + #[cfg(any(target_vendor = "apple", target_os = "linux"))] + for flag in &["-g", "--group"] { + new_ucmd!() + .arg(flag) + .succeeds() + .stdout_is(expected_result(&[flag], false)); + } } #[test] @@ -110,13 +118,22 @@ fn test_id_groups() { let result = scene.ucmd().arg("-G").succeeds(); let groups = result.stdout_str().trim().split_whitespace(); for s in groups { - assert!(s.parse::().is_ok()); + assert!(s.parse::().is_ok()); } let result = scene.ucmd().arg("--groups").succeeds(); let groups = result.stdout_str().trim().split_whitespace(); for s in groups { - assert!(s.parse::().is_ok()); + assert!(s.parse::().is_ok()); + } + + #[cfg(any(target_vendor = "apple", target_os = "linux"))] + for args in &["-G", "--groups"] { + let expect = expected_result(&[args], false); + let actual = new_ucmd!().arg(&args).succeeds().stdout_move_str(); + let mut v_actual: Vec<&str> = actual.split_whitespace().collect(); + let mut v_expect: Vec<&str> = expect.split_whitespace().collect(); + assert_eq!(v_actual.sort_unstable(), v_expect.sort_unstable()); } } @@ -126,11 +143,19 @@ fn test_id_user() { let result = scene.ucmd().arg("-u").succeeds(); let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + assert!(s1.parse::().is_ok()); let result = scene.ucmd().arg("--user").succeeds(); let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + assert!(s1.parse::().is_ok()); + + #[cfg(any(target_vendor = "apple", target_os = "linux"))] + for flag in &["-u", "--user"] { + new_ucmd!() + .arg(flag) + .succeeds() + .stdout_is(expected_result(&[flag], false)); + } } #[test] @@ -167,3 +192,89 @@ fn test_id_password_style() { assert!(result.stdout_str().starts_with(&username)); } + +#[test] +#[cfg(any(target_vendor = "apple", target_os = "linux"))] +fn test_id_default_format() { + // -ugG + for flag in &["--name", "--real"] { + new_ucmd!() + .arg(flag) + .fails() + .stderr_is(expected_result(&[flag], true)); + for &opt in &["--user", "--group", "--groups"] { + if is_ci() && *flag == "--name" { + // '--name' does not work in CI: + // id: cannot find name for user ID 1001 + // id: cannot find name for group ID 116 + println!("test skipped:"); + continue; + } + let args = [opt, flag]; + let expect = expected_result(&args, false); + let actual = new_ucmd!().args(&args).succeeds().stdout_move_str(); + let mut v_actual: Vec<&str> = actual.split_whitespace().collect(); + let mut v_expect: Vec<&str> = expect.split_whitespace().collect(); + assert_eq!(v_actual.sort_unstable(), v_expect.sort_unstable()); + } + } +} + +#[test] +#[cfg(any(target_vendor = "apple", target_os = "linux"))] +fn test_id_zero() { + for z_flag in &["-z", "--zero"] { + for &opt in &["-n", "--name", "-r", "--real"] { + let args = [opt, z_flag]; + new_ucmd!() + .args(&args) + .fails() + .stderr_is(expected_result(&args, true)); + } + for &opt in &["-u", "--user", "-g", "--group"] { + let args = [opt, z_flag]; + new_ucmd!() + .args(&args) + .succeeds() + .stdout_is(expected_result(&args, false)); + } + // '--groups' ids are in no particular order and when paired with '--zero' there's no + // delimiter which makes the split_whitespace-collect-into-vector comparison impossible. + for opt in &["-G", "--groups"] { + let args = [opt, z_flag]; + let result = new_ucmd!().args(&args).succeeds().stdout_move_str(); + assert!(!result.contains(" ")); + assert!(result.ends_with('\0')); + } + } +} + +#[cfg(any(target_vendor = "apple", target_os = "linux"))] +fn expected_result(args: &[&str], exp_fail: bool) -> String { + #[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) + .succeeds() + .stdout_move_str() + } else { + TestScenario::new(&util_name) + .cmd_keepenv(util_name) + .env("LANGUAGE", "C") + .args(args) + .fails() + .stderr_move_str() + }; + // #[cfg(target_vendor = "apple")] + return if cfg!(target_os = "macos") && result.starts_with("gid") { + result[1..].to_string() + } else { + result + }; +} From 026570ff9c887b4c3e2fac56a213ae7e59f59f38 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Wed, 9 Jun 2021 13:24:28 +0200 Subject: [PATCH 03/12] id: add more tests for '--zero' * fix clippy warnings --- src/uu/id/src/id.rs | 1 + tests/by-util/test_id.rs | 100 +++++++++++++++++++++++++++++---------- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 360f6a09c..f0659faf3 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -177,6 +177,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!(1, "cannot print only names or real IDs in default format"); } if (zflag) && !(uflag || gflag || gsflag) { + // GNU testsuite "id/zero.sh" needs this stderr output crash!(1, "option --zero not permitted in default format"); } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index a9c7e31ae..a885069a1 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -6,8 +6,12 @@ use crate::common::util::*; // // 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 +// stderr: "id: Could not find uid 1001: No such id: 1001" +// +// 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" +// fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { if !result.succeeded() { println!("result.stdout = {}", result.stdout_str()); @@ -191,53 +195,97 @@ fn test_id_password_style() { #[test] #[cfg(any(target_vendor = "apple", target_os = "linux"))] fn test_id_default_format() { + // These are the same tests like in test_id_zero but without --zero flag. let scene = TestScenario::new(util_name!()); - // -ugG - for flag in &["--name", "--real"] { + for &opt1 in &["--name", "--real"] { + // id: cannot print only names or real IDs in default format + let args = [opt1]; scene .ucmd() - .arg(flag) + .args(&args) .fails() - .stderr_is(expected_result(&[flag], true)); - for &opt in &["--user", "--group", "--groups"] { - if is_ci() && *flag == "--name" { + .stderr_only(expected_result(&args, true)); + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G n/r + let args = [opt2, opt1]; + let result = scene.ucmd().args(&args).run(); + + if !result.succeeded() && is_ci() + // && (result.stderr_str().contains("cannot find name for") + // || result.stdout_str().contains("cannot find name for")) + { // '--name' does not work on CICD ubuntu-16/ubuntu-18 // id: cannot find name for user ID 1001 // id: cannot find name for group ID 116 - println!("test skipped"); - continue; + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(expected_result(&args, true)); + } else { + result.stdout_only(expected_result(&args, false)); } - let args = [opt, flag]; - scene - .ucmd() - .args(&args) - .succeeds() - .stdout_is(expected_result(&args, false)); } } + // u/g/G + for &opt2 in &["--user", "--group", "--groups"] { + let args = [opt2]; + scene + .ucmd() + .args(&args) + .succeeds() + .stdout_only(expected_result(&args, false)); + } } #[test] #[cfg(any(target_vendor = "apple", target_os = "linux"))] fn test_id_zero() { + let scene = TestScenario::new(util_name!()); for z_flag in &["-z", "--zero"] { - for &opt in &["-n", "--name", "-r", "--real"] { - let args = [opt, z_flag]; - new_ucmd!() + 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_is(expected_result(&args, true)); + .stderr_only(expected_result(&args, true)); + 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(); + + if !result.succeeded() && is_ci() + // && (result.stderr_str().contains("cannot find name for") + // || result.stdout_str().contains("cannot find name for")) + { + // '--name' does not work on CICD ubuntu-16/ubuntu-18 + // id: cannot find name for user ID 1001 + // id: cannot find name for group ID 116 + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(expected_result(&args, true)); + } else { + result.stdout_only(expected_result(&args, false)); + } + } } - for &opt in &["-u", "--user", "-g", "--group", "-G", "--groups"] { - let args = [opt, z_flag]; - new_ucmd!() + // u/g/G z + for &opt2 in &["--user", "--group", "--groups"] { + let args = [opt2, z_flag]; + scene + .ucmd() .args(&args) .succeeds() - .stdout_is(expected_result(&args, false)); + .stdout_only(expected_result(&args, false)); } } } +#[allow(clippy::needless_borrow)] #[cfg(any(target_vendor = "apple", target_os = "linux"))] fn expected_result(args: &[&str], exp_fail: bool) -> String { #[cfg(target_os = "linux")] @@ -260,9 +308,9 @@ fn expected_result(args: &[&str], exp_fail: bool) -> String { .fails() .stderr_move_str() }; - return if cfg!(target_os = "macos") && result.starts_with("gid") { + if cfg!(target_os = "macos") && result.starts_with("gid") { result[1..].to_string() } else { result - }; + } } From 00c05b8687cca0d8a38dc3ad79e93857490f5799 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Thu, 10 Jun 2021 00:19:23 +0200 Subject: [PATCH 04/12] id: add error handling (stderr/exit_code) for '-ugG' --- src/uu/id/src/id.rs | 25 +++++++++++++++++++------ tests/by-util/test_id.rs | 14 ++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index f0659faf3..570a87790 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -201,6 +201,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; let line_ending = if zflag { '\0' } else { '\n' }; + let mut exit_code = 0; if gflag { let id = possible_pw @@ -209,13 +210,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { print!( "{}{}", if nflag { - entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) + 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() }, line_ending ); - return 0; + return exit_code; } if uflag { @@ -225,13 +230,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { print!( "{}{}", if nflag { - entries::uid2usr(id).unwrap_or_else(|_| id.to_string()) + entries::uid2usr(id).unwrap_or_else(|_| { + show_error!("cannot find name for user ID {}", id); + exit_code = 1; + id.to_string() + }) } else { id.to_string() }, line_ending ); - return 0; + return exit_code; } if gsflag { @@ -246,7 +255,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .unwrap_or_else(|| entries::get_groups_gnu(Some(id)).unwrap()) .iter() .map(|&id| if nflag { - entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) + 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() }) @@ -254,7 +267,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .join(delimiter), line_ending ); - return 0; + return exit_code; } if matches.is_present(options::OPT_PASSWORD) { diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index a885069a1..e8e2f8d35 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -209,10 +209,9 @@ fn test_id_default_format() { // u/g/G n/r let args = [opt2, opt1]; let result = scene.ucmd().args(&args).run(); - - if !result.succeeded() && is_ci() - // && (result.stderr_str().contains("cannot find name for") - // || result.stdout_str().contains("cannot find name for")) + if !result.succeeded() + && is_ci() + && result.stderr_str().contains("cannot find name for") { // '--name' does not work on CICD ubuntu-16/ubuntu-18 // id: cannot find name for user ID 1001 @@ -255,10 +254,9 @@ fn test_id_zero() { // u/g/G n/r z let args = [opt2, z_flag, opt1]; let result = scene.ucmd().args(&args).run(); - - if !result.succeeded() && is_ci() - // && (result.stderr_str().contains("cannot find name for") - // || result.stdout_str().contains("cannot find name for")) + if !result.succeeded() + && is_ci() + && result.stderr_str().contains("cannot find name for") { // '--name' does not work on CICD ubuntu-16/ubuntu-18 // id: cannot find name for user ID 1001 From 44d1790d1fee2e9272ab38d854291a97b359299c Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Thu, 10 Jun 2021 10:33:22 +0200 Subject: [PATCH 05/12] id: add more tests for '--zero' --- tests/by-util/test_id.rs | 189 +++++++++------------------------------ tests/common/util.rs | 16 ++++ 2 files changed, 59 insertions(+), 146 deletions(-) diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index e8e2f8d35..bfed78886 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -5,8 +5,9 @@ use crate::common::util::*; // 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" -// stderr: "id: Could not find uid 1001: No such id: 1001" +// 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)" @@ -94,69 +95,6 @@ fn test_id_name_from_id() { 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()); - - result = scene.ucmd().arg("--group").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); - - #[cfg(any(target_vendor = "apple", target_os = "linux"))] - for flag in &["-g", "--group"] { - new_ucmd!() - .arg(flag) - .succeeds() - .stdout_is(expected_result(&[flag], false)); - } -} - -#[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"] { - 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)); - } - } -} - -#[test] -fn test_id_user() { - 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()); - - #[cfg(any(target_vendor = "apple", target_os = "linux"))] - for flag in &["-u", "--user"] { - new_ucmd!() - .arg(flag) - .succeeds() - .stdout_is(expected_result(&[flag], false)); - } -} - #[test] fn test_id_pretty_print() { let username = return_whoami_username(); @@ -193,52 +131,13 @@ fn test_id_password_style() { } #[test] -#[cfg(any(target_vendor = "apple", target_os = "linux"))] +#[cfg(unix)] fn test_id_default_format() { - // These are the same tests like in test_id_zero but without --zero flag. - 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(expected_result(&args, true)); - for &opt2 in &["--user", "--group", "--groups"] { - // u/g/G n/r - let args = [opt2, opt1]; - let result = scene.ucmd().args(&args).run(); - if !result.succeeded() - && is_ci() - && result.stderr_str().contains("cannot find name for") - { - // '--name' does not work on CICD ubuntu-16/ubuntu-18 - // id: cannot find name for user ID 1001 - // id: cannot find name for group ID 116 - scene - .ucmd() - .args(&args) - .fails() - .stderr_only(expected_result(&args, true)); - } else { - result.stdout_only(expected_result(&args, false)); - } - } - } - // u/g/G - for &opt2 in &["--user", "--group", "--groups"] { - let args = [opt2]; - scene - .ucmd() - .args(&args) - .succeeds() - .stdout_only(expected_result(&args, false)); - } + // TODO: These are the same tests like in test_id_zero but without --zero flag. } #[test] -#[cfg(any(target_vendor = "apple", target_os = "linux"))] +#[cfg(unix)] fn test_id_zero() { let scene = TestScenario::new(util_name!()); for z_flag in &["-z", "--zero"] { @@ -249,26 +148,15 @@ fn test_id_zero() { .ucmd() .args(&args) .fails() - .stderr_only(expected_result(&args, true)); + .stderr_only(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(); - if !result.succeeded() - && is_ci() - && result.stderr_str().contains("cannot find name for") - { - // '--name' does not work on CICD ubuntu-16/ubuntu-18 - // id: cannot find name for user ID 1001 - // id: cannot find name for group ID 116 - scene - .ucmd() - .args(&args) - .fails() - .stderr_only(expected_result(&args, true)); - } else { - result.stdout_only(expected_result(&args, false)); - } + let expected_result = expected_result(&args); + result + .stdout_is_bytes(expected_result.stdout()) + .stderr_is_bytes(expected_result.stderr()); } } // u/g/G z @@ -278,37 +166,46 @@ fn test_id_zero() { .ucmd() .args(&args) .succeeds() - .stdout_only(expected_result(&args, false)); + .stdout_only_bytes(expected_result(&args).stdout()); } } } #[allow(clippy::needless_borrow)] -#[cfg(any(target_vendor = "apple", target_os = "linux"))] -fn expected_result(args: &[&str], exp_fail: bool) -> String { +#[cfg(unix)] +fn expected_result(args: &[&str]) -> CmdResult { #[cfg(target_os = "linux")] let util_name = util_name!(); - #[cfg(target_vendor = "apple")] + #[cfg(all(unix, not(target_os = "linux")))] 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) - .succeeds() - .stdout_move_str() - } else { - TestScenario::new(&util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) - .fails() - .stderr_move_str() - }; - if cfg!(target_os = "macos") && result.starts_with("gid") { - result[1..].to_string() - } else { - result + let result = TestScenario::new(&util_name) + .cmd_keepenv(&util_name) + .env("LANGUAGE", "C") + .args(args) + .run(); + + let mut _o = 0; + let mut _e = 0; + #[cfg(all(unix, not(target_os = "linux")))] + { + _o = if result.stdout_str().starts_with(&util_name) { + 1 + } else { + 0 + }; + _e = if result.stderr_str().starts_with(&util_name) { + 1 + } else { + 0 + }; } + + CmdResult::new( + Some(result.tmpd()), + Some(result.code()), + result.succeeded(), + &result.stdout()[_o..], + &result.stderr()[_e..], + ) } diff --git a/tests/common/util.rs b/tests/common/util.rs index 2f7d7dcc4..64485c0a8 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -69,6 +69,22 @@ pub struct CmdResult { } impl CmdResult { + pub fn new( + tmpd: Option>, + code: Option, + success: bool, + stdout: &[u8], + stderr: &[u8], + ) -> CmdResult { + CmdResult { + tmpd, + code, + success, + stdout: stdout.to_vec(), + stderr: stderr.to_vec(), + } + } + /// Returns a reference to the program's standard output as a slice of bytes pub fn stdout(&self) -> &[u8] { &self.stdout From d99385e58520722f32741b3df92c299bd7dd89e3 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Fri, 11 Jun 2021 12:00:26 +0200 Subject: [PATCH 06/12] id: add more tests for multiple and/or invalid usernames --- tests/by-util/test_id.rs | 210 ++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 90 deletions(-) diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index bfed78886..4823cf6d0 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,8 +1,6 @@ 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)" // whoami: cannot find name for user ID 1001 @@ -13,97 +11,99 @@ use crate::common::util::*; // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" // whoami: "runner" // -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 -} -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(""); - } - - result.stdout_str().trim().to_string() +fn whoami() -> String { + std::env::var("USER").unwrap_or_else(|e| { + println!("warning: {}, using \"nobody\" instead", e); + "nobody".to_string() + }) } #[test] -fn test_id() { - let scene = TestScenario::new(util_name!()); +#[cfg(unix)] +fn test_id_no_argument() { + let result = new_ucmd!().run(); + let expected_result = expected_result(&[]); + let mut exp_stdout = expected_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; - } + // uu_stid does not support selinux context. Remove 'context' part from exp_stdout: + let context_offset = expected_result + .stdout_str() + .find(" context") + .unwrap_or(exp_stdout.len()); + exp_stdout.replace_range(context_offset.., "\n"); 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(expected_result.stderr_str()) + .code_is(expected_result.code()); } #[test] -fn test_id_name_from_id() { - let result = new_ucmd!().arg("-nu").run(); - - 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); +#[cfg(unix)] +fn test_id_single_user() { + let args = &[&whoami()[..]]; + let result = new_ucmd!().args(args).run(); + let expected_result = expected_result(args); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()) + .code_is(expected_result.code()); } #[test] -fn test_id_pretty_print() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } +#[cfg(unix)] +fn test_id_single_invalid_user() { + let args = &["hopefully_non_existing_username"]; + let result = new_ucmd!().args(args).run(); + let expected_result = expected_result(args); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()) + .code_is(expected_result.code()); +} +#[test] +#[cfg(unix)] +fn test_id_name() { let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-p").run(); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--name"]; + let result = scene.ucmd().args(&args).run(); + let expected_result = expected_result(&args); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()) + .code_is(expected_result.code()); + + if opt == "--user" { + assert_eq!(result.stdout_str().trim_end(), whoami()); + } + } +} + +#[test] +#[cfg(unix)] +fn test_id_real() { + let scene = TestScenario::new(util_name!()); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--real"]; + let result = scene.ucmd().args(&args).run(); + let expected_result = expected_result(&args); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()) + .code_is(expected_result.code()); + } +} + +#[test] +#[cfg(all(unix, not(target_os = "linux")))] +fn test_id_pretty_print() { + // `-p` is BSD only and not supported on GNU's `id` + let username = whoami(); + + 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)` @@ -113,20 +113,17 @@ fn test_id_pretty_print() { // 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)); } @@ -136,6 +133,39 @@ fn test_id_default_format() { // TODO: These are the same tests like in test_id_zero but without --zero flag. } +#[test] +#[cfg(unix)] +fn test_id_multiple_users() { + // Same typical users that GNU testsuite is using. + let test_users = ["root", "man", "postfix", "sshd", &whoami()]; + + let result = new_ucmd!().args(&test_users).run(); + let expected_result = expected_result(&test_users); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()); +} + +#[test] +#[cfg(unix)] +fn test_id_multiple_invalid_users() { + let test_users = [ + "root", + "hopefully_non_existing_username1", + "man", + "postfix", + "sshd", + "hopefully_non_existing_username2", + &whoami(), + ]; + + let result = new_ucmd!().args(&test_users).run(); + let expected_result = expected_result(&test_users); + result + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()); +} + #[test] #[cfg(unix)] fn test_id_zero() { @@ -155,8 +185,8 @@ fn test_id_zero() { let result = scene.ucmd().args(&args).run(); let expected_result = expected_result(&args); result - .stdout_is_bytes(expected_result.stdout()) - .stderr_is_bytes(expected_result.stderr()); + .stdout_is(expected_result.stdout_str()) + .stderr_is(expected_result.stderr_str()); } } // u/g/G z @@ -166,7 +196,7 @@ fn test_id_zero() { .ucmd() .args(&args) .succeeds() - .stdout_only_bytes(expected_result(&args).stdout()); + .stdout_only(expected_result(&args).stdout_str()); } } } From 60124b8fbd5e620aea227aeb09f85a1a4293705c Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 13 Jun 2021 10:09:24 +0200 Subject: [PATCH 07/12] CICD/GNU: only run/compile tests for `id` --- .github/workflows/CICD.yml | 3 ++- .github/workflows/GNU.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 32c3537c2..42c448561 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -564,7 +564,8 @@ jobs: run: | ## Dependent VARs setup # * determine sub-crate utility list - UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" + # UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" + UTILITY_LIST="id" # TODO: remove after debugging CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo "-puu_${u}"; done;)" echo set-output name=UTILITY_LIST::${UTILITY_LIST} echo ::set-output name=CARGO_UTILITY_LIST_OPTIONS::${CARGO_UTILITY_LIST_OPTIONS} diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index 1f9250900..e9227e38e 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -45,7 +45,8 @@ jobs: - name: Run GNU tests shell: bash run: | - bash uutils/util/run-gnu-test.sh + bash uutils/util/run-gnu-test.sh tests/id/uid.sh # TODO: remove after debugging + bash uutils/util/run-gnu-test.sh tests/id/zero.sh # TODO: remove after debugging - name: Extract tests info shell: bash run: | From 9af93437456fda9e50a9d577315995900b868c92 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 13 Jun 2021 10:12:00 +0200 Subject: [PATCH 08/12] uucore: entries: add documentation --- src/uucore/src/lib/features/entries.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index b94abbe4f..2d5b44362 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -179,6 +179,13 @@ 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 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(). pub fn belongs_to(&self) -> Vec { let mut ngroups: c_int = 8; let mut groups = Vec::with_capacity(ngroups as usize); From 17c6f4c13a3d58a502b51d56bd3fc7416de0a998 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 13 Jun 2021 11:11:04 +0200 Subject: [PATCH 09/12] id: add more tests to be consistent with GNU testsuite tests (tests/id/zero.sh) --- tests/by-util/test_id.rs | 239 +++++++++++++++++++++++++++++++++------ 1 file changed, 207 insertions(+), 32 deletions(-) diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 4823cf6d0..9e1a218ea 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -13,6 +13,8 @@ use crate::common::util::*; // fn whoami() -> 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!("warning: {}, using \"nobody\" instead", e); "nobody".to_string() @@ -21,12 +23,12 @@ fn whoami() -> String { #[test] #[cfg(unix)] -fn test_id_no_argument() { +fn test_id_no_specified_user() { let result = new_ucmd!().run(); let expected_result = expected_result(&[]); let mut exp_stdout = expected_result.stdout_str().to_string(); - // uu_stid does not support selinux context. Remove 'context' part from exp_stdout: + // uu_id does not support selinux context. Remove 'context' part from exp_stdout: let context_offset = expected_result .stdout_str() .find(" context") @@ -42,18 +44,63 @@ fn test_id_no_argument() { #[test] #[cfg(unix)] fn test_id_single_user() { - let args = &[&whoami()[..]]; - let result = new_ucmd!().args(args).run(); - let expected_result = expected_result(args); - result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()) - .code_is(expected_result.code()); + let test_users = [&whoami()[..]]; + + let scene = TestScenario::new(util_name!()); + let mut exp_result = expected_result(&test_users); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .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 = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.pop(); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } } #[test] #[cfg(unix)] -fn test_id_single_invalid_user() { +fn test_id_single_user_non_existing() { let args = &["hopefully_non_existing_username"]; let result = new_ucmd!().args(args).run(); let expected_result = expected_result(args); @@ -127,43 +174,164 @@ fn test_id_password_style() { assert!(result.stdout_str().starts_with(&username)); } -#[test] -#[cfg(unix)] -fn test_id_default_format() { - // TODO: These are the same tests like in test_id_zero but without --zero flag. -} - #[test] #[cfg(unix)] fn test_id_multiple_users() { // Same typical users that GNU testsuite is using. let test_users = ["root", "man", "postfix", "sshd", &whoami()]; - let result = new_ucmd!().args(&test_users).run(); - let expected_result = expected_result(&test_users); - result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()); + let scene = TestScenario::new(util_name!()); + let mut exp_result = expected_result(&test_users); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .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 = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.pop(); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } } #[test] #[cfg(unix)] -fn test_id_multiple_invalid_users() { +fn test_id_multiple_users_non_existing() { let test_users = [ "root", "hopefully_non_existing_username1", + &whoami(), "man", + "hopefully_non_existing_username2", + "hopefully_non_existing_username3", "postfix", "sshd", - "hopefully_non_existing_username2", + "hopefully_non_existing_username4", &whoami(), ]; - let result = new_ucmd!().args(&test_users).run(); - let expected_result = expected_result(&test_users); - result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()); + let scene = TestScenario::new(util_name!()); + let mut exp_result = expected_result(&test_users); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .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 = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + args.pop(); + exp_result = expected_result(&args); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .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(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 = 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(expected_result(&args).stdout_str()); + } } #[test] @@ -171,6 +339,12 @@ fn test_id_multiple_invalid_users() { 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(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]; @@ -183,14 +357,15 @@ fn test_id_zero() { // u/g/G n/r z let args = [opt2, z_flag, opt1]; let result = scene.ucmd().args(&args).run(); - let expected_result = expected_result(&args); + let exp_result = expected_result(&args); result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()); + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } } - // u/g/G z for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G z let args = [opt2, z_flag]; scene .ucmd() From b4c47cc5bda717c60cf38250baea253c4eca13a2 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 13 Jun 2021 11:12:53 +0200 Subject: [PATCH 10/12] id: make `id` pass GNU's testssuite for "tests/id/uid.sh" and "tests/id/zero.sh" --- src/uu/id/src/id.rs | 479 ++++++++++++++++++++++++++++---------------- 1 file changed, 305 insertions(+), 174 deletions(-) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 570a87790..6afb23d67 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -6,15 +6,23 @@ // 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 is not based on coreutils (8.32) GNU's `id`. -// This is based on BSD's `id` (noticeable in functionality, usage text, options text, etc.) +// * This was partially rewritten in order for stdout/stderr/exit_code +// to be conform with GNU coreutils (8.32) testsuite for `id`. +// +// * This passes GNU's coreutils Testsuite (8.32.161-370c2-dirty) +// for "tests/id/uid.sh" and "tests/id/zero/sh". +// +// * Option '--zero' does not exist for BSD's `id`, therefor '--zero' is only +// allowed together with other options that are available on GNU's `id`. +// +// * Help text based on BSD's `id`. // -// Option '--zero' does not exist for BSD's `id`, therefor '--zero' is only allowed together -// with other options that are available on GNU's `id`. // spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag @@ -37,49 +45,12 @@ 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 = "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\nIf 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 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"; @@ -92,21 +63,76 @@ mod options { } 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 behaviour 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(options::OPT_AUDIT) .short("A") - .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, which requires privilege (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(options::OPT_EFFECTIVE_USER) @@ -125,8 +151,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 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."), + .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) @@ -137,7 +172,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 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. If any of the ID numbers cannot be mapped into names, the number will be displayed as usual."), + .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) @@ -148,13 +187,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 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."), + .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;\nnot permitted in default format"), + .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) @@ -164,129 +215,173 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) .get_matches_from(args); - let nflag = matches.is_present(options::OPT_NAME); - let uflag = matches.is_present(options::OPT_EFFECTIVE_USER); - let gflag = matches.is_present(options::OPT_GROUP); - let gsflag = matches.is_present(options::OPT_GROUPS); - let rflag = matches.is_present(options::OPT_REAL_ID); - let zflag = matches.is_present(options::OPT_ZERO); - - // "default format" is when none of '-ugG' was used - // could not implement these "required" rules with just clap - if (nflag || rflag) && !(uflag || gflag || gsflag) { - crash!(1, "cannot print only names or real IDs in default format"); - } - if (zflag) && !(uflag || gflag || gsflag) { - // GNU testsuite "id/zero.sh" needs this stderr output - crash!(1, "option --zero not permitted in default format"); - } - let users: Vec = matches .values_of(options::ARG_USERS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - if matches.is_present(options::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 line_ending = if zflag { '\0' } else { '\n' }; let mut exit_code = 0; - if gflag { - let id = possible_pw - .map(|p| p.gid()) - .unwrap_or(if rflag { getgid() } else { getegid() }); - print!( - "{}{}", - if nflag { - entries::gid2grp(id).unwrap_or_else(|_| { - show_error!("cannot find name for group ID {}", id); + 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; - id.to_string() - }) - } else { - id.to_string() - }, - line_ending - ); - return exit_code; - } + if i + 1 >= users.len() { + break; + } else { + continue; + } + } + } + }; - if uflag { - let id = possible_pw - .map(|p| p.uid()) - .unwrap_or(if rflag { getuid() } else { geteuid() }); - print!( - "{}{}", - if nflag { - entries::uid2usr(id).unwrap_or_else(|_| { - show_error!("cannot find name for user ID {}", id); - exit_code = 1; - id.to_string() - }) - } else { - id.to_string() - }, - line_ending - ); - return exit_code; - } + // 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 gsflag { - let delimiter = if zflag { "\0" } else { " " }; - let id = possible_pw - .map(|p| p.gid()) - .unwrap_or(if rflag { getgid() } else { getegid() }); - print!( - "{}{}", - 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(|_| { - show_error!("cannot find name for group ID {}", id); + 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; - id.to_string() + gid.to_string() }) } else { - id.to_string() - }) - .collect::>() - .join(delimiter), - line_ending - ); - return exit_code; + 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 = if state.user_specified { + possible_pw + .map(|p| p.belongs_to()) + .unwrap_or_else(|| entries::get_groups_gnu(Some(gid)).unwrap()) + } else { + entries::get_groups_gnu(Some(gid)).unwrap() + }; + + 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 matches.is_present(options::OPT_PASSWORD) { - pline(possible_pw.map(|v| v.uid())); - return 0; - }; - - if matches.is_present(options::OPT_HUMAN_READABLE) { - pretty(possible_pw); - return 0; - } - - 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) { @@ -399,30 +494,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() @@ -430,4 +516,49 @@ fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { .collect::>() .join(",") ); + + // placeholder ("-Z" is NotImplemented): + // 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; + } } From 54cbb69d373766e884cf38778da01d8f82344ef1 Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sun, 13 Jun 2021 15:39:31 +0200 Subject: [PATCH 11/12] id/tests: fix tests if run on macOS --- .github/workflows/CICD.yml | 1 + .github/workflows/GNU.yml | 6 + src/uu/id/src/id.rs | 13 +-- tests/by-util/test_id.rs | 232 +++++++++++++++++++++++-------------- 4 files changed, 160 insertions(+), 92 deletions(-) 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/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index e9227e38e..1202de87f 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -45,8 +45,14 @@ jobs: - name: Run GNU tests shell: bash run: | + # bash uutils/util/run-gnu-test.sh + bash uutils/util/run-gnu-test.sh tests/id/context.sh # TODO: remove after debugging + sudo bash uutils/util/run-gnu-test.sh tests/id/setgid.sh # TODO: remove after debugging + bash uutils/util/run-gnu-test.sh tests/id/smack.sh # TODO: remove after debugging bash uutils/util/run-gnu-test.sh tests/id/uid.sh # TODO: remove after debugging bash uutils/util/run-gnu-test.sh tests/id/zero.sh # TODO: remove after debugging + bash uutils/util/run-gnu-test.sh tests/id/no-context.sh # TODO: remove after debugging + bash uutils/util/run-gnu-test.sh tests/id/gnu-zero-uids.sh # todo: remove after debugging - name: Extract tests info shell: bash run: | diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 6afb23d67..35f641b3f 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -24,7 +24,7 @@ // * Help text based on BSD's `id`. // -// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag +// 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)] @@ -92,7 +92,7 @@ struct State { rflag: bool, // --real zflag: bool, // --zero ids: Option, - // The behaviour for calling GNU's `id` and calling GNU's `id $USER` is similar but different. + // 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`. @@ -336,12 +336,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ); } + let groups = entries::get_groups_gnu(Some(gid)).unwrap(); let groups = if state.user_specified { - possible_pw - .map(|p| p.belongs_to()) - .unwrap_or_else(|| entries::get_groups_gnu(Some(gid)).unwrap()) + possible_pw.map(|p| p.belongs_to()).unwrap() } else { - entries::get_groups_gnu(Some(gid)).unwrap() + groups.clone() }; if state.gsflag { @@ -517,7 +516,7 @@ fn id_print(state: &State, groups: Vec) { .join(",") ); - // placeholder ("-Z" is NotImplemented): + // NOTE: placeholder ("-Z" is NotImplemented): // if !state.user_specified { // // print SElinux context (does not depend on "-Z") // print!(" context={}", get_selinux_contexts().join(":")); diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 9e1a218ea..4c41e3131 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -12,11 +12,29 @@ use crate::common::util::*; // whoami: "runner" // +// spell-checker:ignore (ToDO) testsuite coreutil + +const VERSION_EXPECTED: &str = "8.30"; // 8.32 +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; + } + } + }; +} + fn whoami() -> String { - // Use environment variable to get current user instead of invoking `whoami` - // and fall back to user "nobody" on error. + // 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!("warning: {}, using \"nobody\" instead", e); + println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); "nobody".to_string() }) } @@ -25,20 +43,23 @@ fn whoami() -> String { #[cfg(unix)] fn test_id_no_specified_user() { let result = new_ucmd!().run(); - let expected_result = expected_result(&[]); - let mut exp_stdout = expected_result.stdout_str().to_string(); + let exp_result = unwrap_or_return!(expected_result(&[])); + let mut _exp_stdout = exp_result.stdout_str().to_string(); - // uu_id does not support selinux context. Remove 'context' part from exp_stdout: - let context_offset = expected_result - .stdout_str() - .find(" context") - .unwrap_or(exp_stdout.len()); - exp_stdout.replace_range(context_offset.., "\n"); + #[cfg(target_os = "linux")] + { + // NOTE: Strip 'context' part from exp_stdout (remove if SElinux gets added): + let context_offset = exp_result + .stdout_str() + .find(" context=") + .unwrap_or_else(|| _exp_stdout.len()); + _exp_stdout.replace_range(context_offset.., "\n"); + } result - .stdout_is(exp_stdout) - .stderr_is(expected_result.stderr_str()) - .code_is(expected_result.code()); + .stdout_is(_exp_stdout) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } #[test] @@ -47,53 +68,53 @@ fn test_id_single_user() { let test_users = [&whoami()[..]]; let scene = TestScenario::new(util_name!()); - let mut exp_result = expected_result(&test_users); + 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()) + .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 = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--zero"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--name"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.pop(); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); } } @@ -103,11 +124,16 @@ fn test_id_single_user() { fn test_id_single_user_non_existing() { let args = &["hopefully_non_existing_username"]; let result = new_ucmd!().args(args).run(); - let expected_result = expected_result(args); + let exp_result = unwrap_or_return!(expected_result(args)); + + // coreutils 8.32: $ LC_ALL=C id foobar + // macOS: stderr: "id: 'foobar': no such user: Invalid argument" + // linux: stderr: "id: 'foobar': no such user" + // It is unkown why the output on macOS is different. result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()) - .code_is(expected_result.code()); + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); } #[test] @@ -117,11 +143,11 @@ fn test_id_name() { for &opt in &["--user", "--group", "--groups"] { let args = [opt, "--name"]; let result = scene.ucmd().args(&args).run(); - let expected_result = expected_result(&args); + let exp_result = unwrap_or_return!(expected_result(&args)); result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()) - .code_is(expected_result.code()); + .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()); @@ -136,11 +162,11 @@ fn test_id_real() { for &opt in &["--user", "--group", "--groups"] { let args = [opt, "--real"]; let result = scene.ucmd().args(&args).run(); - let expected_result = expected_result(&args); + let exp_result = unwrap_or_return!(expected_result(&args)); result - .stdout_is(expected_result.stdout_str()) - .stderr_is(expected_result.stderr_str()) - .code_is(expected_result.code()); + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } } @@ -159,7 +185,6 @@ fn test_id_pretty_print() { // stdout = // stderr = ', tests/common/util.rs:157:13 println!("test skipped:"); - return; } else { result.success().stdout_contains(username); } @@ -181,53 +206,53 @@ fn test_id_multiple_users() { let test_users = ["root", "man", "postfix", "sshd", &whoami()]; let scene = TestScenario::new(util_name!()); - let mut exp_result = expected_result(&test_users); + 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()) + .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 = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--zero"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--name"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.pop(); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); } } @@ -249,53 +274,53 @@ fn test_id_multiple_users_non_existing() { ]; let scene = TestScenario::new(util_name!()); - let mut exp_result = expected_result(&test_users); + 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()) + .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 = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--zero"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.push("--name"); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); args.pop(); - exp_result = expected_result(&args); + 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()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) .code_is(exp_result.code()); } } @@ -311,12 +336,12 @@ fn test_id_default_format() { .ucmd() .args(&args) .fails() - .stderr_only(expected_result(&args).stderr_str()); + .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 = expected_result(&args); + let exp_result = unwrap_or_return!(expected_result(&args)); result .stdout_is(exp_result.stdout_str()) .stderr_is(exp_result.stderr_str()) @@ -330,7 +355,7 @@ fn test_id_default_format() { .ucmd() .args(&args) .succeeds() - .stdout_only(expected_result(&args).stdout_str()); + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); } } @@ -344,7 +369,7 @@ fn test_id_zero() { .ucmd() .args(&[z_flag]) .fails() - .stderr_only(expected_result(&[z_flag]).stderr_str()); + .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]; @@ -352,12 +377,12 @@ fn test_id_zero() { .ucmd() .args(&args) .fails() - .stderr_only(expected_result(&args).stderr_str()); + .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 = expected_result(&args); + let exp_result = unwrap_or_return!(expected_result(&args)); result .stdout_is(exp_result.stdout_str()) .stderr_is(exp_result.stderr_str()) @@ -371,46 +396,83 @@ fn test_id_zero() { .ucmd() .args(&args) .succeeds() - .stdout_only(expected_result(&args).stdout_str()); + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); } } } #[allow(clippy::needless_borrow)] #[cfg(unix)] -fn expected_result(args: &[&str]) -> CmdResult { +fn expected_result(args: &[&str]) -> Result { + // version for reference coreutil binary + #[cfg(target_os = "linux")] let util_name = util_name!(); #[cfg(all(unix, not(target_os = "linux")))] let util_name = format!("g{}", util_name!()); - let result = TestScenario::new(&util_name) + let scene = TestScenario::new(&util_name); + let version_check = scene + .cmd_keepenv(&util_name) + .env("LANGUAGE", "C") + .arg("--version") + .run(); + let version_check_string: String = version_check + .stdout_str() + .split('\n') + .collect::>() + .get(0) + .map_or_else( + || format!("{}: unexpected output format for reference coreutils '{} --version'", UUTILS_WARNING, util_name), + |s| { + if s.contains(&format!("(GNU coreutils) {}", VERSION_EXPECTED)) { + s.to_string() + } else if s.contains("(GNU coreutils)") { + // example: id (GNU coreutils) 8.32.162-4eda + 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) + } + }, + ); + if version_check_string.starts_with(UUTILS_WARNING) { + return Err(version_check_string); + } + println!("{}", version_check_string); + + let result = scene .cmd_keepenv(&util_name) .env("LANGUAGE", "C") .args(args) .run(); - let mut _o = 0; - let mut _e = 0; - #[cfg(all(unix, not(target_os = "linux")))] - { - _o = if result.stdout_str().starts_with(&util_name) { - 1 - } else { - 0 - }; - _e = if result.stderr_str().starts_with(&util_name) { - 1 - } else { - 0 - }; - } + // #[cfg(all(unix, not(target_os = "linux")))] + // if cfg!(target_os = "macos") { + 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), + ) + }; - CmdResult::new( + Ok(CmdResult::new( Some(result.tmpd()), Some(result.code()), result.succeeded(), - &result.stdout()[_o..], - &result.stderr()[_e..], - ) + stdout.as_bytes(), + stderr.as_bytes(), + )) } From 39aa5312edd768f88a8307adcfc339ae5f20c41a Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Wed, 16 Jun 2021 20:45:46 +0200 Subject: [PATCH 12/12] id/tests: skip tests for multiple_user feature if there's not at least coreutils `id` version 8.31 in `$PATH` --- .github/workflows/GNU.yml | 9 +--- src/uu/id/src/id.rs | 10 ++-- tests/by-util/test_id.rs | 111 +++++++++++++++++++++++--------------- 3 files changed, 76 insertions(+), 54 deletions(-) diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index 1202de87f..1f9250900 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -45,14 +45,7 @@ jobs: - name: Run GNU tests shell: bash run: | - # bash uutils/util/run-gnu-test.sh - bash uutils/util/run-gnu-test.sh tests/id/context.sh # TODO: remove after debugging - sudo bash uutils/util/run-gnu-test.sh tests/id/setgid.sh # TODO: remove after debugging - bash uutils/util/run-gnu-test.sh tests/id/smack.sh # TODO: remove after debugging - bash uutils/util/run-gnu-test.sh tests/id/uid.sh # TODO: remove after debugging - bash uutils/util/run-gnu-test.sh tests/id/zero.sh # TODO: remove after debugging - bash uutils/util/run-gnu-test.sh tests/id/no-context.sh # TODO: remove after debugging - bash uutils/util/run-gnu-test.sh tests/id/gnu-zero-uids.sh # todo: remove after debugging + bash uutils/util/run-gnu-test.sh - name: Extract tests info shell: bash run: | diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 35f641b3f..9037745eb 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -15,13 +15,15 @@ // * This was partially rewritten in order for stdout/stderr/exit_code // to be conform with GNU coreutils (8.32) testsuite for `id`. // -// * This passes GNU's coreutils Testsuite (8.32.161-370c2-dirty) +// * 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`, therefor '--zero' is only +// * 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`. +// * 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 zflag testsuite @@ -516,7 +518,7 @@ fn id_print(state: &State, groups: Vec) { .join(",") ); - // NOTE: placeholder ("-Z" is NotImplemented): + // NOTE: (SELinux NotImplemented) placeholder: // if !state.user_specified { // // print SElinux context (does not depend on "-Z") // print!(" context={}", get_selinux_contexts().join(":")); diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 4c41e3131..b4b929a2c 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,20 +1,15 @@ use crate::common::util::*; -// 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" -// - // spell-checker:ignore (ToDO) testsuite coreutil -const VERSION_EXPECTED: &str = "8.30"; // 8.32 +// 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"; @@ -31,6 +26,17 @@ macro_rules! unwrap_or_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" + // 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| { @@ -48,12 +54,10 @@ fn test_id_no_specified_user() { #[cfg(target_os = "linux")] { - // NOTE: Strip 'context' part from exp_stdout (remove if SElinux gets added): - let context_offset = exp_result - .stdout_str() - .find(" context=") - .unwrap_or_else(|| _exp_stdout.len()); - _exp_stdout.replace_range(context_offset.., "\n"); + // 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 @@ -126,10 +130,10 @@ fn test_id_single_user_non_existing() { 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" - // It is unkown why the output on macOS is different. result .stdout_is(exp_result.stdout_str()) .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) @@ -202,6 +206,16 @@ fn test_id_password_style() { #[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; + } + // Same typical users that GNU testsuite is using. let test_users = ["root", "man", "postfix", "sshd", &whoami()]; @@ -260,6 +274,16 @@ fn test_id_multiple_users() { #[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", @@ -401,58 +425,61 @@ fn test_id_zero() { } } -#[allow(clippy::needless_borrow)] -#[cfg(unix)] -fn expected_result(args: &[&str]) -> Result { - // version for reference coreutil binary - - #[cfg(target_os = "linux")] - let util_name = util_name!(); - #[cfg(all(unix, not(target_os = "linux")))] - let util_name = format!("g{}", util_name!()); - - let scene = TestScenario::new(&util_name); +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(); - let version_check_string: String = version_check + version_check .stdout_str() .split('\n') .collect::>() .get(0) .map_or_else( - || format!("{}: unexpected output format for reference coreutils '{} --version'", UUTILS_WARNING, util_name), + || format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name), |s| { - if s.contains(&format!("(GNU coreutils) {}", VERSION_EXPECTED)) { + if s.contains(&format!("(GNU coreutils) {}", version_expected)) { s.to_string() } else if s.contains("(GNU coreutils)") { - // example: id (GNU coreutils) 8.32.162-4eda let version_found = s.split_whitespace().last().unwrap()[..4].parse::().unwrap_or_default(); - let version_expected = VERSION_EXPECTED.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) + 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) } + 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) + .cmd_keepenv(util_name) .env("LANGUAGE", "C") .args(args) .run(); - // #[cfg(all(unix, not(target_os = "linux")))] - // if cfg!(target_os = "macos") { let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { ( result.stdout_str().to_string(),