1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-29 12:07:46 +00:00

id: make id pass GNU's testssuite for "tests/id/uid.sh" and

"tests/id/zero.sh"
This commit is contained in:
Jan Scheer 2021-06-13 11:12:53 +02:00
parent 17c6f4c13a
commit b4c47cc5bd

View file

@ -6,15 +6,23 @@
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // 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://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 // 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 was partially rewritten in order for stdout/stderr/exit_code
// This is based on BSD's `id` (noticeable in functionality, usage text, options text, etc.) // 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 // 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"))] static ABOUT: &str = "Print user and group information for each specified USER,
mod audit { or (when USER omitted) for the current user.";
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.";
mod options { mod options {
pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this 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_EFFECTIVE_USER: &str = "user";
pub const OPT_GROUP: &str = "group"; pub const OPT_GROUP: &str = "group";
pub const OPT_GROUPS: &str = "groups"; pub const OPT_GROUPS: &str = "groups";
@ -92,21 +63,76 @@ mod options {
} }
fn get_usage() -> String { 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<Ids>,
// 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 { pub fn uumain(args: impl uucore::Args) -> i32 {
let usage = get_usage(); let usage = get_usage();
let after_help = get_description();
let matches = App::new(executable!()) let matches = App::new(executable!())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.usage(&usage[..]) .usage(&usage[..])
.after_help(&after_help[..])
.arg( .arg(
Arg::with_name(options::OPT_AUDIT) Arg::with_name(options::OPT_AUDIT)
.short("A") .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]) .conflicts_with_all(&[
.help("Display the process audit user ID and other process audit properties, which requires privilege (not available on Linux)."), 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(
Arg::with_name(options::OPT_EFFECTIVE_USER) 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) Arg::with_name(options::OPT_GROUPS)
.short("G") .short("G")
.long(options::OPT_GROUPS) .long(options::OPT_GROUPS)
.conflicts_with_all(&[options::OPT_GROUP, options::OPT_EFFECTIVE_USER, options::OPT_HUMAN_READABLE, options::OPT_PASSWORD, options::OPT_AUDIT]) .conflicts_with_all(&[
.help("Display only the different group IDs as white-space separated numbers, in no particular order."), 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(
Arg::with_name(options::OPT_HUMAN_READABLE) 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) Arg::with_name(options::OPT_NAME)
.short("n") .short("n")
.long(options::OPT_NAME) .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(
Arg::with_name(options::OPT_PASSWORD) 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) Arg::with_name(options::OPT_REAL_ID)
.short("r") .short("r")
.long(options::OPT_REAL_ID) .long(options::OPT_REAL_ID)
.help("Display the real ID for the -G, -g and -u options instead of the effective ID."), .help(
"Display the real ID for the -G, -g and -u options instead of \
the effective ID.",
),
) )
.arg( .arg(
Arg::with_name(options::OPT_ZERO) Arg::with_name(options::OPT_ZERO)
.short("z") .short("z")
.long(options::OPT_ZERO) .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(
Arg::with_name(options::ARG_USERS) Arg::with_name(options::ARG_USERS)
@ -164,129 +215,173 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
) )
.get_matches_from(args); .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<String> = matches let users: Vec<String> = matches
.values_of(options::ARG_USERS) .values_of(options::ARG_USERS)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(); .unwrap_or_default();
if matches.is_present(options::OPT_AUDIT) { let mut state = State {
auditid(); nflag: matches.is_present(options::OPT_NAME),
return 0; 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() { let delimiter = {
None if state.zflag {
} else { "\0".to_string()
match Passwd::locate(users[0].as_str()) { } else {
Ok(p) => Some(p), " ".to_string()
Err(_) => crash!(1, "No such user/group: {}", users[0]), }
};
let line_ending = {
if state.zflag {
'\0'
} else {
'\n'
} }
}; };
let line_ending = if zflag { '\0' } else { '\n' };
let mut exit_code = 0; let mut exit_code = 0;
if gflag { for i in 0..=users.len() {
let id = possible_pw let possible_pw = if !state.user_specified {
.map(|p| p.gid()) None
.unwrap_or(if rflag { getgid() } else { getegid() }); } else {
print!( match Passwd::locate(users[i].as_str()) {
"{}{}", Ok(p) => Some(p),
if nflag { Err(_) => {
entries::gid2grp(id).unwrap_or_else(|_| { show_error!("{}: no such user", users[i]);
show_error!("cannot find name for group ID {}", id);
exit_code = 1; exit_code = 1;
id.to_string() if i + 1 >= users.len() {
}) break;
} else { } else {
id.to_string() continue;
}, }
line_ending }
); }
return exit_code; };
}
if uflag { // GNU's `id` does not support the flags: -p/-P/-A.
let id = possible_pw if matches.is_present(options::OPT_PASSWORD) {
.map(|p| p.uid()) // BSD's `id` ignores all but the first specified user
.unwrap_or(if rflag { getuid() } else { geteuid() }); pline(possible_pw.map(|v| v.uid()));
print!( return exit_code;
"{}{}", };
if nflag { if matches.is_present(options::OPT_HUMAN_READABLE) {
entries::uid2usr(id).unwrap_or_else(|_| { // BSD's `id` ignores all but the first specified user
show_error!("cannot find name for user ID {}", id); pretty(possible_pw);
exit_code = 1; return exit_code;
id.to_string() }
}) if matches.is_present(options::OPT_AUDIT) {
} else { // BSD's `id` ignores specified users
id.to_string() auditid();
}, return exit_code;
line_ending }
);
return exit_code;
}
if gsflag { let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or((
let delimiter = if zflag { "\0" } else { " " }; if state.rflag { getuid() } else { geteuid() },
let id = possible_pw if state.rflag { getgid() } else { getegid() },
.map(|p| p.gid()) ));
.unwrap_or(if rflag { getgid() } else { getegid() }); state.ids = Some(Ids {
print!( uid,
"{}{}", gid,
possible_pw euid: geteuid(),
.map(|p| p.belongs_to()) egid: getegid(),
.unwrap_or_else(|| entries::get_groups_gnu(Some(id)).unwrap()) });
.iter()
.map(|&id| if nflag { if state.gflag {
entries::gid2grp(id).unwrap_or_else(|_| { print!(
show_error!("cannot find name for group ID {}", id); "{}",
if state.nflag {
entries::gid2grp(gid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gid);
exit_code = 1; exit_code = 1;
id.to_string() gid.to_string()
}) })
} else { } else {
id.to_string() gid.to_string()
}) }
.collect::<Vec<_>>() );
.join(delimiter), }
line_ending
); if state.uflag {
return exit_code; 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::<Vec<_>>()
.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) { exit_code
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
} }
fn pretty(possible_pw: Option<Passwd>) { fn pretty(possible_pw: Option<Passwd>) {
@ -399,30 +494,21 @@ fn auditid() {
println!("asid={}", auditinfo.ai_asid); println!("asid={}", auditinfo.ai_asid);
} }
fn id_print(possible_pw: Option<Passwd>, p_euid: bool, p_egid: bool) { fn id_print(state: &State, groups: Vec<u32>) {
let (uid, gid) = possible_pw let uid = state.ids.as_ref().unwrap().uid;
.map(|p| (p.uid(), p.gid())) let gid = state.ids.as_ref().unwrap().gid;
.unwrap_or((getuid(), getgid())); let euid = state.ids.as_ref().unwrap().euid;
let egid = state.ids.as_ref().unwrap().egid;
let groups = match Passwd::locate(uid) {
Ok(p) => p.belongs_to(),
Err(e) => crash!(1, "Could not find uid {}: {}", uid, e),
};
print!("uid={}({})", uid, entries::uid2usr(uid).unwrap()); print!("uid={}({})", uid, entries::uid2usr(uid).unwrap());
print!(" gid={}({})", gid, entries::gid2grp(gid).unwrap()); print!(" gid={}({})", gid, entries::gid2grp(gid).unwrap());
if !state.user_specified && (euid != uid) {
let euid = geteuid();
if p_euid && (euid != uid) {
print!(" euid={}({})", euid, entries::uid2usr(euid).unwrap()); print!(" euid={}({})", euid, entries::uid2usr(euid).unwrap());
} }
if !state.user_specified && (egid != gid) {
let egid = getegid();
if p_egid && (egid != gid) {
print!(" egid={}({})", euid, entries::gid2grp(egid).unwrap()); print!(" egid={}({})", euid, entries::gid2grp(egid).unwrap());
} }
print!(
println!(
" groups={}", " groups={}",
groups groups
.iter() .iter()
@ -430,4 +516,49 @@ fn id_print(possible_pw: Option<Passwd>, p_euid: bool, p_egid: bool) {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(",") .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;
}
} }