1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
// This file is part of the uutils coreutils package.
//
// (c) Jian Zeng <anonymousknight96@gmail.com>
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups egid

//! Get password/group file entry
//!
//! # Examples:
//!
//! ```
//! use uucore::entries::{self, Locate};
//!
//! let root_group = if cfg!(any(target_os = "linux", target_os = "android")) {
//!     "root"
//! } else {
//!     "wheel"
//! };
//!
//! assert_eq!("root", entries::uid2usr(0).unwrap());
//! assert_eq!(0, entries::usr2uid("root").unwrap());
//! assert!(entries::gid2grp(0).is_ok());
//! assert!(entries::grp2gid(root_group).is_ok());
//!
//! assert!(entries::Passwd::locate(0).is_ok());
//! assert!(entries::Passwd::locate("0").is_ok());
//! assert!(entries::Passwd::locate("root").is_ok());
//!
//! assert!(entries::Group::locate(0).is_ok());
//! assert!(entries::Group::locate("0").is_ok());
//! assert!(entries::Group::locate(root_group).is_ok());
//! ```

#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
use libc::time_t;
use libc::{c_char, c_int, gid_t, uid_t};
#[cfg(not(target_os = "redox"))]
use libc::{getgrgid, getgrnam, getgroups};
use libc::{getpwnam, getpwuid, group, passwd};

use std::ffi::{CStr, CString};
use std::io::Error as IOError;
use std::io::ErrorKind;
use std::io::Result as IOResult;
use std::ptr;
use std::sync::Mutex;

use once_cell::sync::Lazy;

extern "C" {
    /// From: `<https://man7.org/linux/man-pages/man3/getgrouplist.3.html>`
    /// > The getgrouplist() function scans the group database to obtain
    /// > the list of groups that user belongs to.
    fn getgrouplist(
        name: *const c_char,
        gid: gid_t,
        groups: *mut gid_t,
        ngroups: *mut c_int,
    ) -> c_int;
}

/// From: `<https://man7.org/linux/man-pages/man2/getgroups.2.html>`
/// > getgroups() returns the supplementary group IDs of the calling
/// > process in list.
/// > If size is zero, list is not modified, but the total number of
/// > supplementary group IDs for the process is returned.  This allows
/// > the caller to determine the size of a dynamically allocated list
/// > to be used in a further call to getgroups().
#[cfg(not(target_os = "redox"))]
pub fn get_groups() -> IOResult<Vec<gid_t>> {
    let mut groups = Vec::new();
    loop {
        let ngroups = match unsafe { getgroups(0, ptr::null_mut()) } {
            -1 => return Err(IOError::last_os_error()),
            // Not just optimization; 0 would mess up the next call
            0 => return Ok(Vec::new()),
            n => n,
        };

        // This is a small buffer, so we can afford to zero-initialize it and
        // use safe Vec operations
        groups.resize(ngroups.try_into().unwrap(), 0);
        let res = unsafe { getgroups(ngroups, groups.as_mut_ptr()) };
        if res == -1 {
            let err = IOError::last_os_error();
            if err.raw_os_error() == Some(libc::EINVAL) {
                // Number of groups changed, retry
                continue;
            } else {
                return Err(err);
            }
        } else {
            groups.truncate(ngroups.try_into().unwrap());
            return Ok(groups);
        }
    }
}

/// The list of group IDs returned from GNU's `groups` and GNU's `id --groups`
/// starts with the effective group ID (egid).
/// This is a wrapper for `get_groups()` to mimic this behavior.
///
/// If `arg_id` is `None` (default), `get_groups_gnu` moves the effective
/// group id (egid) to the first entry in the returned Vector.
/// If `arg_id` is `Some(x)`, `get_groups_gnu` moves the id with value `x`
/// to the first entry in the returned Vector. This might be necessary
/// for `id --groups --real` if `gid` and `egid` are not equal.
///
/// From: `<https://www.man7.org/linux/man-pages/man3/getgroups.3p.html>`
/// > As implied by the definition of supplementary groups, the
/// > effective group ID may appear in the array returned by
/// > getgroups() or it may be returned only by getegid().  Duplication
/// > may exist, but the application needs to call getegid() to be sure
/// > of getting all of the information. Various implementation
/// > variations and administrative sequences cause the set of groups
/// > appearing in the result of getgroups() to vary in order and as to
/// > whether the effective group ID is included, even when the set of
/// > groups is the same (in the mathematical sense of ``set''). (The
/// > history of a process and its parents could affect the details of
/// > the result.)
#[cfg(all(unix, not(target_os = "redox"), feature = "process"))]
pub fn get_groups_gnu(arg_id: Option<u32>) -> IOResult<Vec<gid_t>> {
    let groups = get_groups()?;
    let egid = arg_id.unwrap_or_else(crate::features::process::getegid);
    Ok(sort_groups(groups, egid))
}

#[cfg(all(unix, feature = "process"))]
fn sort_groups(mut groups: Vec<gid_t>, egid: gid_t) -> Vec<gid_t> {
    if let Some(index) = groups.iter().position(|&x| x == egid) {
        groups[..=index].rotate_right(1);
    } else {
        groups.insert(0, egid);
    }
    groups
}

#[derive(Clone, Debug)]
pub struct Passwd {
    /// AKA passwd.pw_name
    pub name: String,
    /// AKA passwd.pw_uid
    pub uid: uid_t,
    /// AKA passwd.pw_gid
    pub gid: gid_t,
    /// AKA passwd.pw_gecos
    pub user_info: Option<String>,
    /// AKA passwd.pw_shell
    pub user_shell: Option<String>,
    /// AKA passwd.pw_dir
    pub user_dir: Option<String>,
    /// AKA passwd.pw_passwd
    pub user_passwd: Option<String>,
    /// AKA passwd.pw_class
    #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
    pub user_access_class: Option<String>,
    /// AKA passwd.pw_change
    #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
    pub passwd_change_time: time_t,
    /// AKA passwd.pw_expire
    #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
    pub expiration: time_t,
}

/// SAFETY: ptr must point to a valid C string.
/// Returns None if ptr is null.
unsafe fn cstr2string(ptr: *const c_char) -> Option<String> {
    if ptr.is_null() {
        None
    } else {
        Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
    }
}

impl Passwd {
    /// SAFETY: All the pointed-to strings must be valid and not change while
    /// the function runs. That means PW_LOCK must be held.
    unsafe fn from_raw(raw: passwd) -> Self {
        Self {
            name: cstr2string(raw.pw_name).expect("passwd without name"),
            uid: raw.pw_uid,
            gid: raw.pw_gid,
            #[cfg(not(all(
                target_os = "android",
                any(target_arch = "x86", target_arch = "arm")
            )))]
            user_info: cstr2string(raw.pw_gecos),
            #[cfg(all(target_os = "android", any(target_arch = "x86", target_arch = "arm")))]
            user_info: None,
            user_shell: cstr2string(raw.pw_shell),
            user_dir: cstr2string(raw.pw_dir),
            user_passwd: cstr2string(raw.pw_passwd),
            #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
            user_access_class: cstr2string(raw.pw_class),
            #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
            passwd_change_time: raw.pw_change,
            #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
            expiration: raw.pw_expire,
        }
    }

    /// This is a wrapper function for `libc::getgrouplist`.
    ///
    /// From: `<https://man7.org/linux/man-pages/man3/getgrouplist.3.html>`
    /// > If the number of groups of which user is a member is less than or
    /// > equal to *ngroups, then the value *ngroups is returned.
    /// > If the user is a member of more than *ngroups groups, then
    /// > getgrouplist() returns -1.  In this case, the value returned in
    /// > *ngroups can be used to resize the buffer passed to a further
    /// > call getgrouplist().
    ///
    /// However, on macOS/darwin (and maybe others?) `getgrouplist` does
    /// not update `ngroups` if `ngroups` is too small. Therefore, if not
    /// updated by `getgrouplist`, `ngroups` needs to be increased in a
    /// loop until `getgrouplist` stops returning -1.
    pub fn belongs_to(&self) -> Vec<gid_t> {
        let mut ngroups: c_int = 8;
        let mut ngroups_old: c_int;
        let mut groups = vec![0; ngroups.try_into().unwrap()];
        let name = CString::new(self.name.clone()).unwrap();
        loop {
            ngroups_old = ngroups;
            if unsafe { getgrouplist(name.as_ptr(), self.gid, groups.as_mut_ptr(), &mut ngroups) }
                == -1
            {
                if ngroups == ngroups_old {
                    ngroups *= 2;
                }
                groups.resize(ngroups.try_into().unwrap(), 0);
            } else {
                break;
            }
        }
        let ngroups = ngroups.try_into().unwrap();
        assert!(ngroups <= groups.len());
        groups.truncate(ngroups);
        groups
    }
}

#[derive(Clone, Debug)]
pub struct Group {
    /// AKA group.gr_name
    pub name: String,
    /// AKA group.gr_gid
    pub gid: gid_t,
}

impl Group {
    /// SAFETY: gr_name must be valid and not change while
    /// the function runs. That means PW_LOCK must be held.
    unsafe fn from_raw(raw: group) -> Self {
        Self {
            name: cstr2string(raw.gr_name).expect("group without name"),
            gid: raw.gr_gid,
        }
    }
}

/// Fetch desired entry.
pub trait Locate<K> {
    fn locate(key: K) -> IOResult<Self>
    where
        Self: ::std::marker::Sized;
}

// These functions are not thread-safe:
// > The return value may point to a static area, and may be
// > overwritten by subsequent calls to getpwent(3), getpwnam(),
// > or getpwuid().
// This applies not just to the struct but also the strings it points
// to, so we must copy all the data we want before releasing the lock.
// (Technically we must also ensure that the raw functions aren't being called
// anywhere else in the program.)
static PW_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

macro_rules! f {
    ($fnam:ident, $fid:ident, $t:ident, $st:ident) => {
        impl Locate<$t> for $st {
            fn locate(k: $t) -> IOResult<Self> {
                let _guard = PW_LOCK.lock();
                // SAFETY: We're holding PW_LOCK.
                unsafe {
                    let data = $fid(k);
                    if !data.is_null() {
                        Ok($st::from_raw(ptr::read(data as *const _)))
                    } else {
                        // FIXME: Resource limits, signals and I/O failure may
                        // cause this too. See getpwnam(3).
                        // errno must be set to zero before the call. We can
                        // use libc::__errno_location() on some platforms.
                        // The same applies for the two cases below.
                        Err(IOError::new(
                            ErrorKind::NotFound,
                            format!("No such id: {}", k),
                        ))
                    }
                }
            }
        }

        impl<'a> Locate<&'a str> for $st {
            fn locate(k: &'a str) -> IOResult<Self> {
                let _guard = PW_LOCK.lock();
                if let Ok(id) = k.parse::<$t>() {
                    // SAFETY: We're holding PW_LOCK.
                    unsafe {
                        let data = $fid(id);
                        if !data.is_null() {
                            Ok($st::from_raw(ptr::read(data as *const _)))
                        } else {
                            Err(IOError::new(
                                ErrorKind::NotFound,
                                format!("No such id: {}", id),
                            ))
                        }
                    }
                } else {
                    // SAFETY: We're holding PW_LOCK.
                    unsafe {
                        let cstring = CString::new(k).unwrap();
                        let data = $fnam(cstring.as_ptr());
                        if !data.is_null() {
                            Ok($st::from_raw(ptr::read(data as *const _)))
                        } else {
                            Err(IOError::new(
                                ErrorKind::NotFound,
                                format!("Not found: {}", k),
                            ))
                        }
                    }
                }
            }
        }
    };
}

f!(getpwnam, getpwuid, uid_t, Passwd);
#[cfg(not(target_os = "redox"))]
f!(getgrnam, getgrgid, gid_t, Group);

#[inline]
pub fn uid2usr(id: uid_t) -> IOResult<String> {
    Passwd::locate(id).map(|p| p.name)
}

#[cfg(not(target_os = "redox"))]
#[inline]
pub fn gid2grp(id: gid_t) -> IOResult<String> {
    Group::locate(id).map(|p| p.name)
}

#[inline]
pub fn usr2uid(name: &str) -> IOResult<uid_t> {
    Passwd::locate(name).map(|p| p.uid)
}

#[cfg(not(target_os = "redox"))]
#[inline]
pub fn grp2gid(name: &str) -> IOResult<gid_t> {
    Group::locate(name).map(|p| p.gid)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_sort_groups() {
        assert_eq!(sort_groups(vec![1, 2, 3], 4), vec![4, 1, 2, 3]);
        assert_eq!(sort_groups(vec![1, 2, 3], 3), vec![3, 1, 2]);
        assert_eq!(sort_groups(vec![1, 2, 3], 2), vec![2, 1, 3]);
        assert_eq!(sort_groups(vec![1, 2, 3], 1), vec![1, 2, 3]);
        assert_eq!(sort_groups(vec![1, 2, 3], 0), vec![0, 1, 2, 3]);
    }

    #[test]
    fn test_entries_get_groups_gnu() {
        if let Ok(mut groups) = get_groups() {
            if let Some(last) = groups.pop() {
                groups.insert(0, last);
                assert_eq!(get_groups_gnu(Some(last)).unwrap(), groups);
            }
        }
    }
}