diff --git a/src/unistd.rs b/src/unistd.rs index f422f09198..5de6305265 100644 --- a/src/unistd.rs +++ b/src/unistd.rs @@ -5,7 +5,7 @@ use {Error, Result, NixPath}; use fcntl::{AtFlags, at_rawfd, fcntl, FdFlag, OFlag}; use fcntl::FcntlArg::F_SETFD; use libc::{self, c_char, c_void, c_int, c_long, c_uint, size_t, pid_t, off_t, - uid_t, gid_t, mode_t}; + uid_t, gid_t, mode_t, PATH_MAX}; use std::{fmt, mem, ptr}; use std::ffi::{CString, CStr, OsString, OsStr}; use std::os::unix::ffi::{OsStringExt, OsStrExt}; @@ -20,6 +20,11 @@ pub use self::pivot_root::*; #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "openbsd"))] pub use self::setres::*; +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] +pub use self::usergroupiter::*; /// User identifier /// @@ -534,6 +539,21 @@ pub fn symlinkat( Errno::result(res).map(drop) } +// Double the buffer capacity up to limit. In case it already has +// reached the limit, return Errno::ERANGE. +fn reserve_double_buffer_size(buf: &mut Vec, limit: usize) -> Result<()> { + use std::cmp::min; + + if buf.len() >= limit { + return Err(Error::Sys(Errno::ERANGE)) + } + + let capacity = min(buf.capacity() * 2, limit); + buf.reserve(capacity); + + Ok(()) +} + /// Returns the current directory as a `PathBuf` /// /// Err is returned if the current user doesn't have the permission to read or search a component @@ -576,11 +596,8 @@ pub fn getcwd() -> Result { } } - // Trigger the internal buffer resizing logic of `Vec` by requiring - // more space than the current capacity. - let cap = buf.capacity(); - buf.set_len(cap); - buf.reserve(1); + // Trigger the internal buffer resizing logic. + reserve_double_buffer_size(&mut buf, PATH_MAX as usize)?; } } } @@ -1319,32 +1336,37 @@ pub fn setgid(gid: Gid) -> Result<()> { #[cfg(not(any(target_os = "ios", target_os = "macos")))] pub fn getgroups() -> Result> { // First get the number of groups so we can size our Vec - let ret = unsafe { libc::getgroups(0, ptr::null_mut()) }; + let ngroups = unsafe { libc::getgroups(0, ptr::null_mut()) }; + let mut groups = Vec::::with_capacity(Errno::result(ngroups)? as usize); - // Now actually get the groups. We try multiple times in case the number of - // groups has changed since the first call to getgroups() and the buffer is - // now too small. - let mut groups = Vec::::with_capacity(Errno::result(ret)? as usize); + // The value returned shall always be greater than or equal to one + // and less than or equal to the value of {NGROUPS_MAX} + 1. + let ngroups_max = match sysconf(SysconfVar::NGROUPS_MAX) { + Ok(Some(n)) => (n + 1) as usize, + Ok(None) | Err(_) => ::max_value(), + }; + + // We try multiple times in case the number of groups has changed + // since the first call to getgroups() and the buffer is now too + // small. loop { // FIXME: On the platforms we currently support, the `Gid` struct has // the same representation in memory as a bare `gid_t`. This is not // necessarily the case on all Rust platforms, though. See RFC 1785. - let ret = unsafe { + let ngroups = unsafe { libc::getgroups(groups.capacity() as c_int, groups.as_mut_ptr() as *mut gid_t) }; - match Errno::result(ret) { + match Errno::result(ngroups) { Ok(s) => { unsafe { groups.set_len(s as usize) }; return Ok(groups); }, Err(Error::Sys(Errno::EINVAL)) => { - // EINVAL indicates that the buffer size was too small. Trigger - // the internal buffer resizing logic of `Vec` by requiring - // more space than the current capacity. - let cap = groups.capacity(); - unsafe { groups.set_len(cap) }; - groups.reserve(1); + // EINVAL indicates that the buffer size was too + // small. Trigger the internal buffer resizing logic + reserve_double_buffer_size(&mut groups, ngroups_max) + .or(Err(Error::Sys(Errno::EINVAL)))?; }, Err(e) => return Err(e) } @@ -1462,19 +1484,8 @@ pub fn getgrouplist(user: &CStr, group: Gid) -> Result> { // BSD systems will still fill the groups buffer with as many // groups as possible, but Linux manpages do not mention this // behavior. - - let cap = groups.capacity(); - if cap >= ngroups_max as usize { - // We already have the largest capacity we can, give up - return Err(Error::invalid_argument()); - } - - // Reserve space for at least ngroups - groups.reserve(ngroups as usize); - - // Even if the buffer gets resized to bigger than ngroups_max, - // don't ever ask for more than ngroups_max groups - ngroups = min(ngroups_max, groups.capacity() as c_int); + reserve_double_buffer_size(&mut groups, ngroups_max as usize) + .or(Err(Error::invalid_argument()))?; } } } @@ -2392,3 +2403,431 @@ pub fn access(path: &P, amode: AccessFlags) -> Result<()> { })?; Errno::result(res).map(drop) } + +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] +/// Default buffer size for system user and group querying functions +const PWGRP_BUFSIZE: usize = 1024; + +/// Representation of a User, based on `libc::passwd` +/// +/// The reason some fields in this struct are `String` and others are `CString` is because some +/// fields are based on the user's locale, which could be non-UTF8, while other fields are +/// guaranteed to conform to [`NAME_REGEX`](https://serverfault.com/a/73101/407341), which only +/// contains ASCII. +#[derive(Debug, Clone, PartialEq)] +pub struct User { + /// Username + pub name: String, + /// User password (probably encrypted) + pub passwd: CString, + /// User ID + pub uid: Uid, + /// Group ID + pub gid: Gid, + /// User information + #[cfg(not(target_os = "android"))] + pub gecos: CString, + /// Home directory + pub dir: PathBuf, + /// Path to shell + pub shell: PathBuf, + /// Login class + #[cfg(not(any(target_os = "android", target_os = "linux")))] + pub class: CString, + /// Last password change + #[cfg(not(any(target_os = "android", target_os = "linux")))] + pub change: libc::time_t, + /// Expiration time of account + #[cfg(not(any(target_os = "android", target_os = "linux")))] + pub expire: libc::time_t +} + +impl From<&libc::passwd> for User { + fn from(pw: &libc::passwd) -> User { + unsafe { + User { + name: CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned(), + passwd: CString::new(CStr::from_ptr((*pw).pw_passwd).to_bytes()).unwrap(), + #[cfg(not(target_os = "android"))] + gecos: CString::new(CStr::from_ptr((*pw).pw_gecos).to_bytes()).unwrap(), + dir: PathBuf::from(OsStr::from_bytes(CStr::from_ptr((*pw).pw_dir).to_bytes())), + shell: PathBuf::from(OsStr::from_bytes(CStr::from_ptr((*pw).pw_shell).to_bytes())), + uid: Uid::from_raw((*pw).pw_uid), + gid: Gid::from_raw((*pw).pw_gid), + #[cfg(not(any(target_os = "android", target_os = "linux")))] + class: CString::new(CStr::from_ptr((*pw).pw_class).to_bytes()).unwrap(), + #[cfg(not(any(target_os = "android", target_os = "linux")))] + change: (*pw).pw_change, + #[cfg(not(any(target_os = "android", target_os = "linux")))] + expire: (*pw).pw_expire + } + } + } +} + +impl User { + fn from_anything(f: F) -> Result> + where + F: Fn(*mut libc::passwd, + *mut libc::c_char, + libc::size_t, + *mut *mut libc::passwd) -> libc::c_int + { + let buflimit = 16384; + let bufsize = match sysconf(SysconfVar::GETPW_R_SIZE_MAX) { + Ok(Some(n)) => n as usize, + Ok(None) | Err(_) => buflimit as usize, + }; + + let mut cbuf = Vec::with_capacity(bufsize); + let mut pwd = mem::MaybeUninit::::uninit(); + let mut res = ptr::null_mut(); + + loop { + let error = unsafe { + Errno::clear(); + f(pwd.as_mut_ptr(), cbuf.as_mut_ptr(), cbuf.capacity(), &mut res) + }; + + if error == 0 { + if res.is_null() { + return Ok(None); + } else { + let pwd = unsafe { pwd.assume_init() }; + return Ok(Some(User::from(&pwd))); + } + } else if Errno::last() == Errno::ERANGE { + // Trigger the internal buffer resizing logic. + reserve_double_buffer_size(&mut cbuf, buflimit)?; + } else { + return Err(Error::Sys(Errno::last())); + } + } + } + + /// Get a user by UID. + /// + /// Internally, this function calls + /// [getpwuid_r(3)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getpwuid_r.html) + /// + /// # Examples + /// + /// ``` + /// use nix::unistd::{Uid, User}; + /// // Returns an Result>, thus the double unwrap. + /// let res = User::from_uid(Uid::from_raw(0)).unwrap().unwrap(); + /// assert!(res.name == "root"); + /// ``` + pub fn from_uid(uid: Uid) -> Result> { + User::from_anything(|pwd, cbuf, cap, res| { + unsafe { libc::getpwuid_r(uid.0, pwd, cbuf, cap, res) } + }) + } + + /// Get a user by name. + /// + /// Internally, this function calls + /// [getpwnam_r(3)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getpwuid_r.html) + /// + /// # Examples + /// + /// ``` + /// use nix::unistd::User; + /// // Returns an Result>, thus the double unwrap. + /// let res = User::from_name("root").unwrap().unwrap(); + /// assert!(res.name == "root"); + /// ``` + pub fn from_name(name: &str) -> Result> { + let name = CString::new(name).unwrap(); + User::from_anything(|pwd, cbuf, cap, res| { + unsafe { libc::getpwnam_r(name.as_ptr(), pwd, cbuf, cap, res) } + }) + } +} + +/// Representation of a Group, based on `libc::group` +#[derive(Debug, Clone, PartialEq)] +pub struct Group { + /// Group name + pub name: String, + /// Group ID + pub gid: Gid, + /// List of Group members + pub mem: Vec +} + +impl From<&libc::group> for Group { + fn from(gr: &libc::group) -> Group { + unsafe { + Group { + name: CStr::from_ptr((*gr).gr_name).to_string_lossy().into_owned(), + gid: Gid::from_raw((*gr).gr_gid), + mem: Group::members((*gr).gr_mem) + } + } + } +} + +impl Group { + unsafe fn members(mem: *mut *mut c_char) -> Vec { + let mut ret = Vec::new(); + + for i in 0.. { + let u = mem.offset(i); + if (*u).is_null() { + break; + } else { + let s = CStr::from_ptr(*u).to_string_lossy().into_owned(); + ret.push(s); + } + } + + ret + } + + fn from_anything(f: F) -> Result> + where + F: Fn(*mut libc::group, + *mut libc::c_char, + libc::size_t, + *mut *mut libc::group) -> libc::c_int + { + let buflimit = 16384; + let bufsize = match sysconf(SysconfVar::GETGR_R_SIZE_MAX) { + Ok(Some(n)) => n as usize, + Ok(None) | Err(_) => buflimit as usize, + }; + + let mut cbuf = Vec::with_capacity(bufsize); + let mut grp = mem::MaybeUninit::::uninit(); + let mut res = ptr::null_mut(); + + loop { + let error = unsafe { + Errno::clear(); + f(grp.as_mut_ptr(), cbuf.as_mut_ptr(), cbuf.capacity(), &mut res) + }; + + if error == 0 { + if res.is_null() { + return Ok(None); + } else { + let grp = unsafe { grp.assume_init() }; + return Ok(Some(Group::from(&grp))); + } + } else if Errno::last() == Errno::ERANGE { + // Trigger the internal buffer resizing logic. + reserve_double_buffer_size(&mut cbuf, buflimit)?; + } else { + return Err(Error::Sys(Errno::last())); + } + } + } + + /// Get a group by GID. + /// + /// Internally, this function calls + /// [getgrgid_r(3)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getpwuid_r.html) + /// + /// # Examples + /// + // Disable this test on all OS except Linux as root group may not exist. + #[cfg_attr(not(target_os = "linux"), doc = " ```no_run")] + #[cfg_attr(target_os = "linux", doc = " ```")] + /// use nix::unistd::{Gid, Group}; + /// // Returns an Result>, thus the double unwrap. + /// let res = Group::from_gid(Gid::from_raw(0)).unwrap().unwrap(); + /// assert!(res.name == "root"); + /// ``` + pub fn from_gid(gid: Gid) -> Result> { + Group::from_anything(|grp, cbuf, cap, res| { + unsafe { libc::getgrgid_r(gid.0, grp, cbuf, cap, res) } + }) + } + + /// Get a group by name. + /// + /// Internally, this function calls + /// [getgrnam_r(3)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getpwuid_r.html) + /// + /// # Examples + /// + // Disable this test on all OS except Linux as root group may not exist. + #[cfg_attr(not(target_os = "linux"), doc = " ```no_run")] + #[cfg_attr(target_os = "linux", doc = " ```")] + /// use nix::unistd::Group; + /// // Returns an Result>, thus the double unwrap. + /// let res = Group::from_name("root").unwrap().unwrap(); + /// assert!(res.name == "root"); + /// ``` + pub fn from_name(name: &str) -> Result> { + let name = CString::new(name).unwrap(); + Group::from_anything(|grp, cbuf, cap, res| { + unsafe { libc::getgrnam_r(name.as_ptr(), grp, cbuf, cap, res) } + }) + } +} + +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] +mod usergroupiter { + use libc::{self, c_char}; + use Result; + use errno::Errno; + use super::{Error, User, Group, PWGRP_BUFSIZE}; + use std::{mem, ptr}; + + /// Used to get all of the users on the system. + /// + /// # Examples + /// + /// ``` + /// # use nix::unistd::Users; + /// Users::default() + /// .map(|e| e.map(|pw| println!("{}\t{}", pw.name, pw.uid))) + /// .collect::>(); + /// + /// ``` + /// + /// # Safety + /// + /// This iterator should not be used in different threads without synchronization; while doing so + /// will not cause undefined behavior, because modern systems lack re-entrant versions of + /// `setpwent` and `endpwent`, it is very likely that iterators running in different threads will + /// yield different numbers of items. + #[derive(Debug, Clone, Eq, PartialEq, Hash)] + pub struct Users(usize); + + impl Default for Users { + fn default() -> Self { + Users::with_capacity(PWGRP_BUFSIZE) + } + } + + impl Users { + /// Create a new `Users` instance with given capacity. + pub fn with_capacity(bufsize: usize) -> Self { + unsafe { libc::setpwent(); } + Users(bufsize) + } + + /// Get the buffer size this `Users` instance was created with. + pub fn bufsize(&self) -> usize { + self.0 + } + } + + impl Iterator for Users { + type Item = Result; + fn next(&mut self) -> Option> { + let mut cbuf = vec![0 as c_char; self.0]; + let mut pwd = mem::MaybeUninit::::uninit(); + let mut res = ptr::null_mut(); + + let error = unsafe { + Errno::clear(); + libc::getpwent_r( + pwd.as_mut_ptr(), + cbuf.as_mut_ptr(), + self.0, + &mut res + ) + }; + + let pwd = unsafe { pwd.assume_init() }; + + if error == 0 && !res.is_null() { + Some(Ok(User::from(&pwd))) + } else if error == libc::ERANGE { + Some(Err(Error::Sys(Errno::last()))) + } else { + None + } + } + } + + impl Drop for Users { + fn drop(&mut self) { + unsafe { libc::endpwent() }; + } + } + + /// Used to get all of the groups on the system. + /// + /// # Examples + /// + /// ``` + /// # use nix::unistd::Groups; + /// Groups::default() + /// .map(|e| e.map(|gr| println!("{}\t{}", gr.name, gr.gid))) + /// .collect::>(); + /// ``` + /// + /// # Safety + /// + /// This iterator should not be used in different threads without synchronization; while doing so + /// will not cause undefined behavior, because modern systems lack re-entrant versions of + /// `setgrent` and `endgrent`, it is very likely that iterators running in different threads will + /// yield different numbers of items. + #[derive(Debug, Clone, Eq, PartialEq, Hash)] + pub struct Groups(usize); + + impl Default for Groups { + fn default() -> Self { + Groups::with_capacity(PWGRP_BUFSIZE) + } + } + + impl Groups { + /// Create a new `Groups` instance with given capacity. + pub fn with_capacity(bufsize: usize) -> Self { + unsafe { libc::setgrent(); } + Groups(bufsize) + } + + /// Get the buffer size this `Users` instance was created with. + pub fn bufsize(&self) -> usize { + self.0 + } + } + + impl Iterator for Groups { + type Item = Result; + fn next(&mut self) -> Option> { + let mut cbuf = vec![0 as c_char; self.0]; + let mut grp = mem::MaybeUninit::::uninit(); + let mut res = ptr::null_mut(); + + let error = unsafe { + Errno::clear(); + libc::getgrent_r( + grp.as_mut_ptr(), + cbuf.as_mut_ptr(), + self.0, + &mut res + ) + }; + + let grp = unsafe { grp.assume_init() }; + + if error == 0 && !res.is_null() { + Some(Ok(Group::from(&grp))) + } else if error == libc::ERANGE { + Some(Err(Error::Sys(Errno::last()))) + } else { + None + } + } + } + + impl Drop for Groups { + fn drop(&mut self) { + unsafe { libc::endgrent() }; + } + } +} diff --git a/test/test.rs b/test/test.rs index 6a71d261b5..b682a60485 100644 --- a/test/test.rs +++ b/test/test.rs @@ -120,6 +120,8 @@ lazy_static! { pub static ref PTSNAME_MTX: Mutex<()> = Mutex::new(()); /// Any test that alters signal handling must grab this mutex. pub static ref SIGNAL_MTX: Mutex<()> = Mutex::new(()); + /// Any test that uses the `Users` or `Groups` iterators must grab this mutex. + pub static ref USER_GRP_ITER_MTX: Mutex<()> = Mutex::new(()); } /// RAII object that restores a test's original directory on drop diff --git a/test/test_unistd.rs b/test/test_unistd.rs index 46196dec7c..5d29ebcaab 100644 --- a/test/test_unistd.rs +++ b/test/test_unistd.rs @@ -600,6 +600,50 @@ fn test_symlinkat() { ); } +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] + +#[test] +fn test_users_iterator() { + let _m = ::USER_GRP_ITER_MTX.lock().expect("Mutex got poisoned by another test"); + + let entries = Users::default(); + let users: Vec> = entries.collect(); + let entries2 = Users::default(); + let users2: Vec> = entries2.collect(); + assert!(users == users2 && users.len() > 0); +} + +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] + +#[test] +fn test_groups_iterator() { + let _m = ::USER_GRP_ITER_MTX.lock().expect("Mutex got poisoned by another test"); + + let entries = Groups::default(); + let groups: Vec> = entries.collect(); + let entries2 = Groups::default(); + let groups2: Vec> = entries2.collect(); + assert!(groups == groups2 && groups.len() > 0); +} + +#[cfg(not(any(target_os = "android", + target_os = "ios", + target_os = "macos", + target_env = "musl")))] +#[test] +/// This test sees what happens when we use a ridiculously small buffer. +fn test_users_iterator_smallbuf() { + let _m = ::USER_GRP_ITER_MTX.lock().expect("Mutex got poisoned by another test"); + + let bufsize = 2; + assert!(Users::with_capacity(bufsize).next().unwrap().is_err()); +} #[test] fn test_unlinkat_dir_noremovedir() {