use crate::host::Elevation;
use std::ffi::{CStr, OsStr};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
pub(crate) fn login_name() -> Option<String> {
entry(|passwd| {
text(passwd, passwd.pw_name)?
.to_str()
.ok()
.map(str::to_owned)
})
}
pub(crate) fn home() -> Option<PathBuf> {
#[cfg(test)]
assert!(
testing::reaches_the_real_home(),
"a unit test asked for this account's real home: give it a home of its own, as \
Context::for_unit_test does"
);
if cfg!(all(feature = "test-support", not(test))) {
return None;
}
passwd_home()
}
fn passwd_home() -> Option<PathBuf> {
entry(|passwd| path(passwd, passwd.pw_dir))
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn is_the_accounts_own_home(path: &std::path::Path) -> bool {
let resolved = |p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.into());
passwd_home().is_some_and(|own| resolved(&own) == resolved(path))
}
#[cfg(test)]
pub(crate) mod testing {
use std::cell::Cell;
thread_local! {
static REACHES: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn reaching_the_real_home() -> Reaching {
REACHES.with(|reaches| reaches.set(true));
Reaching(())
}
pub(crate) struct Reaching(());
impl Drop for Reaching {
fn drop(&mut self) {
REACHES.with(|reaches| reaches.set(false));
}
}
pub(super) fn reaches_the_real_home() -> bool {
REACHES.with(Cell::get)
}
}
pub(crate) fn login_shell() -> Option<PathBuf> {
entry(|passwd| path(passwd, passwd.pw_shell))
}
pub(crate) fn elevation(sudo: bool) -> Elevation {
let root = unsafe { libc::geteuid() } == 0;
elevation_of(sudo, root)
}
fn elevation_of(sudo: bool, root: bool) -> Elevation {
match (sudo, root) {
(true, _) => Elevation::Elevated { why: "with sudo" },
(false, true) => Elevation::Elevated { why: "as root" },
(false, false) => Elevation::Normal,
}
}
fn entry<T>(read: impl FnOnce(&libc::passwd) -> Option<T>) -> Option<T> {
grown(
first_size(),
|passwd, buffer, found| {
unsafe {
libc::getpwuid_r(
libc::getuid(),
passwd,
buffer.as_mut_ptr(),
buffer.len(),
found,
)
}
},
read,
)
}
fn first_size() -> usize {
let said = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
usize::try_from(said)
.ok()
.filter(|&size| size > 0)
.unwrap_or(4096)
}
const LARGEST: usize = 1 << 20;
fn grown<T>(
mut size: usize,
mut lookup: impl FnMut(
&mut libc::passwd,
&mut [libc::c_char],
&mut *mut libc::passwd,
) -> libc::c_int,
read: impl FnOnce(&libc::passwd) -> Option<T>,
) -> Option<T> {
loop {
let mut buffer: Vec<libc::c_char> = vec![0; size];
let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut found: *mut libc::passwd = std::ptr::null_mut();
match lookup(&mut passwd, &mut buffer, &mut found) {
0 if !found.is_null() => return read(&passwd),
libc::ERANGE if size < LARGEST => size = size.saturating_mul(2).min(LARGEST),
_ => return None,
}
}
}
fn text(entry: &libc::passwd, field: *const libc::c_char) -> Option<&CStr> {
let _ = entry;
(!field.is_null()).then(|| unsafe { CStr::from_ptr(field) })
}
fn path(entry: &libc::passwd, field: *const libc::c_char) -> Option<PathBuf> {
text(entry, field)
.filter(|named| !named.is_empty())
.map(|named| PathBuf::from(OsStr::from_bytes(named.to_bytes())))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn needing<'a>(
home: &'a str,
sizes: &'a mut Vec<usize>,
) -> impl FnMut(&mut libc::passwd, &mut [libc::c_char], &mut *mut libc::passwd) -> libc::c_int + 'a
{
move |passwd, buffer, found| {
sizes.push(buffer.len());
let bytes = [home.as_bytes(), b"\0"].concat();
if buffer.len() < bytes.len() {
return libc::ERANGE;
}
for (to, from) in buffer.iter_mut().zip(&bytes) {
*to = libc::c_char::from_ne_bytes([*from]);
}
passwd.pw_dir = buffer.as_mut_ptr();
*found = passwd;
0
}
}
#[test]
fn an_entry_too_large_for_the_first_buffer_is_looked_up_again() {
let home = format!("/Users/{}", "x".repeat(5000));
let mut sizes = Vec::new();
let found = grown(4096, needing(&home, &mut sizes), |passwd| {
path(passwd, passwd.pw_dir)
});
assert!(found.as_deref() == Some(Path::new(&home)), "{sizes:?}");
assert_eq!(sizes, [4096, 8192]);
}
#[test]
fn a_lookup_that_never_fits_ends() {
let home = "x".repeat(LARGEST * 2);
let mut sizes = Vec::new();
let found = grown(4096, needing(&home, &mut sizes), |_| Some(()));
assert_eq!(found, None);
assert_eq!(sizes.first(), Some(&4096));
assert_eq!(sizes.last(), Some(&LARGEST));
assert_eq!(sizes.len(), 9, "{sizes:?}");
}
#[test]
fn this_account_has_a_name_and_a_home() {
assert!(login_name().is_some_and(|name| !name.is_empty()));
let _real = testing::reaching_the_real_home();
assert!(
home().is_some_and(|home| home.is_absolute()),
"{:?}",
home()
);
assert_eq!(home(), passwd_home());
assert!(is_the_accounts_own_home(&home().expect("a home")));
assert!(!is_the_accounts_own_home(&std::env::temp_dir()));
}
#[test]
#[should_panic(expected = "a unit test asked for this account's real home")]
fn a_unit_test_never_reaches_the_real_home() {
let _ = home();
}
#[test]
fn reaching_the_real_home_is_said_for_one_thread_while_it_is_kept() {
{
let _real = testing::reaching_the_real_home();
assert!(testing::reaches_the_real_home());
let other = std::thread::spawn(testing::reaches_the_real_home);
assert!(!other.join().expect("it answers"), "another thread");
}
assert!(!testing::reaches_the_real_home(), "once it is dropped");
}
#[test]
fn sudo_is_said_before_root_and_only_neither_is_the_person() {
assert_eq!(
elevation_of(true, true),
Elevation::Elevated { why: "with sudo" }
);
assert_eq!(
elevation_of(true, false),
Elevation::Elevated { why: "with sudo" },
"sudo -u someone else"
);
assert_eq!(
elevation_of(false, true),
Elevation::Elevated { why: "as root" }
);
assert_eq!(elevation_of(false, false), Elevation::Normal);
}
#[test]
fn root_is_read_from_the_effective_user_id() {
use std::os::unix::fs::MetadataExt;
let made = std::env::temp_dir().join(format!("pitboard-euid-{}", std::process::id()));
std::fs::write(&made, b"").expect("a file of this process's own");
let owner = std::fs::metadata(&made).map(|made| made.uid());
let _ = std::fs::remove_file(&made);
let root = owner.expect("the file this process made") == 0;
assert_eq!(elevation(false), elevation_of(false, root));
assert_eq!(elevation(true), Elevation::Elevated { why: "with sudo" });
}
#[test]
fn this_account_has_a_login_shell() {
assert!(
login_shell().is_some_and(|shell| shell.is_absolute()),
"{:?}",
login_shell()
);
}
}