Skip to main content

gander_core/
users.rs

1//! uid to user-name resolution.
2//!
3//! The `USER` column used to print the raw numeric uid, which is not what anyone
4//! wants to read. `getpwuid_r` is POSIX and works on both backends, so it is
5//! preferred over parsing `/etc/passwd` (which cannot see directory-service users
6//! on macOS and would need to be re-implemented for every other platform).
7
8use std::collections::HashMap;
9
10use crate::Pid;
11
12/// Cache of resolved user names, keyed by uid.
13///
14/// Lookups are cached because a scan resolves the same handful of uids thousands
15/// of times, and `getpwuid_r` walks the directory service each call.
16#[derive(Default)]
17pub struct Users {
18    by_uid: HashMap<u32, Name>,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22enum Name {
23    Resolved(String),
24    /// The uid has no passwd entry. Remembered so we do not retry every tick.
25    Unknown,
26}
27
28impl Users {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Name for `uid`, falling back to the decimal number when there is no entry.
34    pub fn name(&mut self, uid: u32) -> &str {
35        let entry = self.by_uid.entry(uid).or_insert_with(|| match lookup(uid) {
36            Some(name) => Name::Resolved(name),
37            None => Name::Unknown,
38        });
39
40        match entry {
41            Name::Resolved(name) => name.as_str(),
42            Name::Unknown => {
43                // Store the numeric form so the borrowed return is stable.
44                *entry = Name::Resolved(uid.to_string());
45                match entry {
46                    Name::Resolved(name) => name.as_str(),
47                    Name::Unknown => unreachable!(),
48                }
49            }
50        }
51    }
52
53    /// Resolve without caching, for one-off use.
54    pub fn name_of(uid: u32) -> String {
55        lookup(uid).unwrap_or_else(|| uid.to_string())
56    }
57
58    pub fn len(&self) -> usize {
59        self.by_uid.len()
60    }
61
62    pub fn is_empty(&self) -> bool {
63        self.by_uid.is_empty()
64    }
65}
66
67/// Ask the system for the login name of `uid`.
68#[cfg(unix)]
69fn lookup(uid: u32) -> Option<String> {
70    // 1 KiB is the documented safe starting size; sysconf(_SC_GETPW_R_SIZE_MAX)
71    // is advisory and sometimes returns -1, so it is only a hint.
72    let mut buf = vec![0i8; 1024];
73
74    loop {
75        let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
76        let mut result: *mut libc::passwd = std::ptr::null_mut();
77
78        // SAFETY: `buf` is writable and `buf.len()` bytes long; `pwd` and `result`
79        // are valid out-parameters. A non-zero return means it failed or the
80        // buffer was too small.
81        let rc = unsafe {
82            libc::getpwuid_r(
83                uid as libc::uid_t,
84                &mut pwd,
85                buf.as_mut_ptr().cast(),
86                buf.len(),
87                &mut result,
88            )
89        };
90
91        if rc == libc::ERANGE && buf.len() < 1 << 20 {
92            buf.resize(buf.len() * 2, 0);
93            continue;
94        }
95        if rc != 0 || result.is_null() {
96            return None;
97        }
98
99        // SAFETY: on success `pw_name` points into `buf`, which outlives this
100        // block, and is NUL-terminated.
101        let name = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
102        return Some(name.to_string_lossy().into_owned());
103    }
104}
105
106#[cfg(not(unix))]
107fn lookup(_uid: u32) -> Option<String> {
108    None
109}
110
111/// Pids whose owner is `uid`, for the user filter.
112pub fn pids_of_user(rows: &[crate::row::Row], uid: u32) -> Vec<Pid> {
113    rows.iter()
114        .filter(|r| r.uid == uid)
115        .map(|r| r.pid)
116        .collect()
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn resolves_root() {
125        // uid 0 exists on every unix.
126        assert_eq!(Users::name_of(0), "root");
127    }
128
129    #[test]
130    fn resolves_the_current_user() {
131        // SAFETY: `getuid` has no preconditions.
132        let uid = unsafe { libc::getuid() };
133        let name = Users::name_of(uid);
134        assert!(!name.is_empty());
135        assert_ne!(
136            name,
137            uid.to_string(),
138            "current user should have a passwd entry"
139        );
140    }
141
142    #[test]
143    fn unknown_uid_falls_back_to_the_number() {
144        // 4_000_000 is above the range any sane system allocates.
145        assert_eq!(Users::name_of(4_000_000), "4,000,000".replace(',', ""));
146    }
147
148    #[test]
149    fn cache_returns_a_stable_borrow() {
150        let mut users = Users::new();
151        let a = users.name(0).to_string();
152        let b = users.name(0).to_string();
153        assert_eq!(a, b);
154        assert_eq!(users.len(), 1, "second lookup must hit the cache");
155    }
156
157    #[test]
158    fn cache_caches_the_fallback_too() {
159        let mut users = Users::new();
160        assert_eq!(users.name(4_000_001), "4000001");
161        assert_eq!(users.name(4_000_001), "4000001");
162        assert_eq!(users.len(), 1);
163    }
164}