diff --git a/examples/example.rs b/examples/example.rs index 97741b8..069d0ef 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,15 +1,15 @@ extern crate users; -use users::{Users, OSUsers}; +use users::{Users, Groups, OSUsers}; fn main() { - let mut cache = OSUsers::empty_cache(); - + let cache = OSUsers::empty_cache(); + let current_uid = cache.get_current_uid(); println!("Your UID is {}", current_uid); - + let you = cache.get_user_by_uid(current_uid).expect("No entry for current user!"); println!("Your username is {}", you.name); - + let primary_group = cache.get_group_by_gid(you.primary_group).expect("No entry for your primary group!"); println!("Your primary group has ID {} and name {}", primary_group.gid, primary_group.name); diff --git a/examples/threading.rs b/examples/threading.rs new file mode 100644 index 0000000..24ac1a3 --- /dev/null +++ b/examples/threading.rs @@ -0,0 +1,61 @@ +//! This example demonstrates how to use an `OSUsers` cache in a +//! multi-threaded situation. The cache uses `RefCell`s internally, so it +//! is distinctly not thread-safe. Instead, you’ll need to place it within +//! some kind of lock in order to have threads access it one-at-a-time. +//! +//! It queries all the users it can find in the range 500..510. This is the +//! default uid range on my Apple laptop -- Linux starts counting from 1000, +//! but I can’t include both in the range! It spawns one thread per user to +//! query, with each thread accessing the same cache. +//! +//! Then, afterwards, it retrieves references to the users that had been +//! cached earlier. + +// For extra fun, try uncommenting some of the lines of code below, making +// the code try to access the users cache *without* a Mutex, and see it +// spew compile errors at you. + +extern crate users; +use users::{Users, OSUsers, uid_t}; + +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::thread; + +const LO: uid_t = 500; +const HI: uid_t = 510; + +fn main() { + + // For thread-safely, our users cache needs to be within a Mutex, so + // only one thread can access it once. This Mutex needs to be within an + // Arc, so multiple threads can access the Mutex. + let cache = Arc::new(Mutex::new(OSUsers::empty_cache())); + // let cache = OSUsers::empty_cache(); + + // Loop over the range and query all the users in the range. Although we + // could use the `&User` values returned, we just ignore them. + for uid in LO .. HI { + let cache = cache.clone(); + + thread::spawn(move || { + let cache = cache.lock().unwrap(); // Unlock the mutex + let _ = cache.get_user_by_uid(uid); // Query our users cache! + }); + } + + // Wait for all the threads to finish. + thread::sleep(Duration::from_millis(100)); + + // Loop over the same range and print out all the users we find. + // These users will be retrieved from the cache. + for uid in LO .. HI { + let cache = cache.lock().unwrap(); // Re-unlock the mutex + if let Some(u) = cache.get_user_by_uid(uid) { // Re-query our cache! + println!("User #{} is {}", u.uid, u.name) + } + else { + println!("User #{} does not exist", uid); + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 426ac39..92a6875 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ //! methods on it. For example: //! //! ```rust -//! use users::{Users, OSUsers}; +//! use users::{Users, Groups, OSUsers}; //! let mut cache = OSUsers::empty_cache(); //! let uid = cache.get_current_uid(); //! let user = cache.get_user_by_uid(uid).unwrap(); @@ -89,11 +89,11 @@ //! And again, a complete example: //! //! ```rust -//! use users::{Users, OSUsers}; +//! use users::{Users, Groups, OSUsers}; //! let mut cache = OSUsers::empty_cache(); //! let group = cache.get_group_by_name("admin").expect("No such group 'admin'!"); //! println!("The '{}' group has the ID {}", group.name, group.gid); -//! for member in group.members.into_iter() { +//! for member in &group.members { //! println!("{} is a member of the group", member); //! } //! ``` @@ -108,14 +108,11 @@ //! Use the mocking module to create custom tables to test your code for these //! edge cases. - -use std::borrow::ToOwned; -use std::collections::hash_map::Entry::{Occupied, Vacant}; -use std::collections::HashMap; use std::ffi::{CStr, CString}; -use std::io; +use std::io::{Error as IOError, Result as IOResult}; use std::ptr::read; use std::str::from_utf8_unchecked; +use std::sync::Arc; extern crate libc; pub use libc::{uid_t, gid_t, c_int}; @@ -127,81 +124,86 @@ use libc::{c_char, time_t}; use libc::c_char; pub mod mock; +pub mod os; +pub use os::OSUsers; - -/// The trait for the `OSUsers` object. +/// Trait for producers of users. pub trait Users { - /// Return a User object if one exists for the given user ID; otherwise, return None. - fn get_user_by_uid(&mut self, uid: uid_t) -> Option; + /// Returns a User if one exists for the given user ID; otherwise, returns None. + fn get_user_by_uid(&self, uid: uid_t) -> Option>; - /// Return a User object if one exists for the given username; otherwise, return None. - fn get_user_by_name(&mut self, username: &str) -> Option; + /// Returns a User if one exists for the given username; otherwise, returns None. + fn get_user_by_name(&self, username: &str) -> Option>; - /// Return a Group object if one exists for the given group ID; otherwise, return None. - fn get_group_by_gid(&mut self, gid: gid_t) -> Option; + /// Returns the user ID for the user running the process. + fn get_current_uid(&self) -> uid_t; - /// Return a Group object if one exists for the given groupname; otherwise, return None. - fn get_group_by_name(&mut self, group_name: &str) -> Option; + /// Returns the username of the user running the process. + fn get_current_username(&self) -> Option>; - /// Return the user ID for the user running the process. - fn get_current_uid(&mut self) -> uid_t; + /// Returns the effective user id. + fn get_effective_uid(&self) -> uid_t; - /// Return the username of the user running the process. - fn get_current_username(&mut self) -> Option; + /// Returns the effective username. + fn get_effective_username(&self) -> Option>; +} - /// Return the group ID for the user running the process. - fn get_current_gid(&mut self) -> gid_t; +/// Trait for producers of groups. +pub trait Groups { - /// Return the group name of the user running the process. - fn get_current_groupname(&mut self) -> Option; + /// Returns a Group object if one exists for the given group ID; otherwise, returns None. + fn get_group_by_gid(&self, gid: gid_t) -> Option>; - /// Return the effective user id. - fn get_effective_uid(&mut self) -> uid_t; + /// Returns a Group object if one exists for the given groupname; otherwise, returns None. + fn get_group_by_name(&self, group_name: &str) -> Option>; - /// Return the effective group id. - fn get_effective_gid(&mut self) -> gid_t; + /// Returns the group ID for the user running the process. + fn get_current_gid(&self) -> gid_t; - /// Return the effective username. - fn get_effective_username(&mut self) -> Option; + /// Returns the group name of the user running the process. + fn get_current_groupname(&self) -> Option>; - /// Return the effective group name. - fn get_effective_groupname(&mut self) -> Option; + /// Returns the effective group id. + fn get_effective_gid(&self) -> gid_t; + + /// Returns the effective group name. + fn get_effective_groupname(&self) -> Option>; } #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] #[repr(C)] struct c_passwd { - pub pw_name: *const c_char, // user name - pub pw_passwd: *const c_char, // password field - pub pw_uid: uid_t, // user ID - pub pw_gid: gid_t, // group ID - pub pw_change: time_t, // password change time - pub pw_class: *const c_char, - pub pw_gecos: *const c_char, - pub pw_dir: *const c_char, // user's home directory - pub pw_shell: *const c_char, // user's shell - pub pw_expire: time_t, // password expiry time + pw_name: *const c_char, // user name + pw_passwd: *const c_char, // password field + pw_uid: uid_t, // user ID + pw_gid: gid_t, // group ID + pw_change: time_t, // password change time + pw_class: *const c_char, + pw_gecos: *const c_char, + pw_dir: *const c_char, // user's home directory + pw_shell: *const c_char, // user's shell + pw_expire: time_t, // password expiry time } #[cfg(target_os = "linux")] #[repr(C)] struct c_passwd { - pub pw_name: *const c_char, // user name - pub pw_passwd: *const c_char, // password field - pub pw_uid: uid_t, // user ID - pub pw_gid: gid_t, // group ID - pub pw_gecos: *const c_char, - pub pw_dir: *const c_char, // user's home directory - pub pw_shell: *const c_char, // user's shell + pw_name: *const c_char, // user name + pw_passwd: *const c_char, // password field + pw_uid: uid_t, // user ID + pw_gid: gid_t, // group ID + pw_gecos: *const c_char, + pw_dir: *const c_char, // user's home directory + pw_shell: *const c_char, // user's shell } #[repr(C)] struct c_group { - pub gr_name: *const c_char, // group name - pub gr_passwd: *const c_char, // password - pub gr_gid: gid_t, // group id - pub gr_mem: *const *const c_char, // names of users in the group + gr_name: *const c_char, // group name + gr_passwd: *const c_char, // password + gr_gid: gid_t, // group id + gr_mem: *const *const c_char, // names of users in the group } extern { @@ -227,15 +229,15 @@ extern { fn setregid(rgid: gid_t, egid: gid_t) -> c_int; } -#[derive(Clone)] /// Information about a particular user. +#[derive(Clone)] pub struct User { /// This user's ID pub uid: uid_t, /// This user's name - pub name: String, + pub name: Arc, /// The ID of this user's primary group pub primary_group: gid_t, @@ -255,27 +257,12 @@ pub struct Group { pub gid: uid_t, /// This group's name - pub name: String, + pub name: Arc, /// Vector of the names of the users who belong to this group as a non-primary member pub members: Vec, } -/// A producer of user and group instances that caches every result. -#[derive(Clone)] -pub struct OSUsers { - users: HashMap>, - users_back: HashMap>, - - groups: HashMap>, - groups_back: HashMap>, - - uid: Option, - gid: Option, - euid: Option, - egid: Option, -} - unsafe fn from_raw_buf(p: *const i8) -> String { from_utf8_unchecked(CStr::from_ptr(p).to_bytes()).to_string() } @@ -285,7 +272,7 @@ unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { let pw = read(pointer); Some(User { uid: pw.pw_uid as uid_t, - name: from_raw_buf(pw.pw_name as *const i8), + name: Arc::new(from_raw_buf(pw.pw_name as *const i8)), primary_group: pw.pw_gid as gid_t, home_dir: from_raw_buf(pw.pw_dir as *const i8), shell: from_raw_buf(pw.pw_shell as *const i8) @@ -301,7 +288,7 @@ unsafe fn struct_to_group(pointer: *const c_group) -> Option { let gr = read(pointer); let name = from_raw_buf(gr.gr_name as *const i8); let members = members(gr.gr_mem); - Some(Group { gid: gr.gr_gid, name: name, members: members }) + Some(Group { gid: gr.gr_gid, name: Arc::new(name), members: members }) } else { None @@ -328,305 +315,137 @@ unsafe fn members(groups: *const *const c_char) -> Vec { } } -impl Users for OSUsers { - fn get_user_by_uid(&mut self, uid: uid_t) -> Option { - match self.users.entry(uid) { - Vacant(entry) => { - let user = unsafe { passwd_to_user(getpwuid(uid)) }; - match user { - Some(user) => { - entry.insert(Some(user.clone())); - self.users_back.insert(user.name.clone(), Some(user.uid)); - Some(user) - }, - None => { - entry.insert(None); - None - } - } - }, - Occupied(entry) => entry.get().clone(), - } - } - fn get_user_by_name(&mut self, username: &str) -> Option { - // to_owned() could change here: - // https://github.com/rust-lang/rfcs/blob/master/text/0509-collections-reform-part-2.md#alternatives-to-toowned-on-entries - match self.users_back.entry(username.to_owned()) { - Vacant(entry) => { - let username_c = CString::new(username); - - if !username_c.is_ok() { - // This usually means the given username contained a '\0' already - // It is debatable what to do here - return None; - } - - let user = unsafe { passwd_to_user(getpwnam(username_c.unwrap().as_ptr())) }; - match user { - Some(user) => { - entry.insert(Some(user.uid)); - self.users.insert(user.uid, Some(user.clone())); - Some(user) - }, - None => { - entry.insert(None); - None - } - } - }, - Occupied(entry) => match entry.get() { - &Some(uid) => self.users[&uid].clone(), - &None => None, - } - } - } - - fn get_group_by_gid(&mut self, gid: gid_t) -> Option { - match self.groups.entry(gid) { - Vacant(entry) => { - let group = unsafe { struct_to_group(getgrgid(gid)) }; - match group { - Some(group) => { - entry.insert(Some(group.clone())); - self.groups_back.insert(group.name.clone(), Some(group.gid)); - Some(group) - }, - None => { - entry.insert(None); - None - } - } - }, - Occupied(entry) => entry.get().clone(), - } - } - - fn get_group_by_name(&mut self, group_name: &str) -> Option { - // to_owned() could change here: - // https://github.com/rust-lang/rfcs/blob/master/text/0509-collections-reform-part-2.md#alternatives-to-toowned-on-entries - match self.groups_back.entry(group_name.to_owned()) { - Vacant(entry) => { - let group_name_c = CString::new(group_name); - - if !group_name_c.is_ok() { - // This usually means the given username contained a '\0' already - // It is debatable what to do here - return None; - } - - let user = unsafe { struct_to_group(getgrnam(group_name_c.unwrap().as_ptr())) }; - match user { - Some(group) => { - entry.insert(Some(group.gid)); - self.groups.insert(group.gid, Some(group.clone())); - Some(group) - }, - None => { - entry.insert(None); - None - } - } - }, - Occupied(entry) => match entry.get() { - &Some(gid) => self.groups[&gid].clone(), - &None => None, - } - } - } - - fn get_current_uid(&mut self) -> uid_t { - match self.uid { - Some(uid) => uid, - None => { - let uid = unsafe { getuid() }; - self.uid = Some(uid); - uid - } - } - } - - /// Return the username of the user running the process. - fn get_current_username(&mut self) -> Option { - let uid = self.get_current_uid(); - self.get_user_by_uid(uid).map(|u| u.name) - } - - fn get_current_gid(&mut self) -> gid_t { - match self.gid { - Some(gid) => gid, - None => { - let gid = unsafe { getgid() }; - self.gid = Some(gid); - gid - } - } - } - - fn get_current_groupname(&mut self) -> Option { - let gid = self.get_current_gid(); - self.get_group_by_gid(gid).map(|g| g.name) - } - - fn get_effective_gid(&mut self) -> gid_t { - match self.egid { - Some(gid) => gid, - None => { - let gid = unsafe { getegid() }; - self.egid = Some(gid); - gid - } - } - } - - fn get_effective_groupname(&mut self) -> Option { - let gid = self.get_effective_gid(); - self.get_group_by_gid(gid).map(|g| g.name) - } - - fn get_effective_uid(&mut self) -> uid_t { - match self.euid { - Some(uid) => uid, - None => { - let uid = unsafe { geteuid() }; - self.euid = Some(uid); - uid - } - } - } - - fn get_effective_username(&mut self) -> Option { - let uid = self.get_effective_uid(); - self.get_user_by_uid(uid).map(|u| u.name) - } -} - -impl OSUsers { - /// Create a new empty OS Users object. - pub fn empty_cache() -> OSUsers { - OSUsers { - users: HashMap::new(), - users_back: HashMap::new(), - groups: HashMap::new(), - groups_back: HashMap::new(), - uid: None, - gid: None, - euid: None, - egid: None, - } - } -} - -/// Return a User object if one exists for the given user ID; otherwise, return None. +/// Returns a User object if one exists for the given user ID; otherwise, return None. pub fn get_user_by_uid(uid: uid_t) -> Option { - OSUsers::empty_cache().get_user_by_uid(uid) + unsafe { passwd_to_user(getpwuid(uid)) } } -/// Return a User object if one exists for the given username; otherwise, return None. +/// Returns a User object if one exists for the given username; otherwise, return None. pub fn get_user_by_name(username: &str) -> Option { - OSUsers::empty_cache().get_user_by_name(username) + let username_c = CString::new(username); + + if !username_c.is_ok() { + // This usually means the given username contained a '\0' already + // It is debatable what to do here + return None; + } + + unsafe { passwd_to_user(getpwnam(username_c.unwrap().as_ptr())) } } -/// Return a Group object if one exists for the given group ID; otherwise, return None. +/// Returns a Group object if one exists for the given group ID; otherwise, return None. pub fn get_group_by_gid(gid: gid_t) -> Option { - OSUsers::empty_cache().get_group_by_gid(gid) + unsafe { struct_to_group(getgrgid(gid)) } } -/// Return a Group object if one exists for the given groupname; otherwise, return None. +/// Returns a Group object if one exists for the given groupname; otherwise, return None. pub fn get_group_by_name(group_name: &str) -> Option { - OSUsers::empty_cache().get_group_by_name(group_name) + let group_name_c = CString::new(group_name); + + if !group_name_c.is_ok() { + // This usually means the given username contained a '\0' already + // It is debatable what to do here + return None; + } + + unsafe { struct_to_group(getgrnam(group_name_c.unwrap().as_ptr())) } } -/// Return the user ID for the user running the process. +/// Returns the user ID for the user running the process. pub fn get_current_uid() -> uid_t { - OSUsers::empty_cache().get_current_uid() + unsafe { getuid() } } -/// Return the username of the user running the process. +/// Returns the username of the user running the process. pub fn get_current_username() -> Option { - OSUsers::empty_cache().get_current_username() + let uid = get_current_uid(); + get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name).unwrap()) } -/// Return the user ID for the effective user running the process. +/// Returns the user ID for the effective user running the process. pub fn get_effective_uid() -> uid_t { - OSUsers::empty_cache().get_effective_uid() + unsafe { geteuid() } } -/// Return the username of the effective user running the process. +/// Returns the username of the effective user running the process. pub fn get_effective_username() -> Option { - OSUsers::empty_cache().get_effective_username() + let uid = get_effective_uid(); + get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name).unwrap()) } -/// Return the group ID for the user running the process. +/// Returns the group ID for the user running the process. pub fn get_current_gid() -> gid_t { - OSUsers::empty_cache().get_current_gid() + unsafe { getgid() } } -/// Return the groupname of the user running the process. +/// Returns the groupname of the user running the process. pub fn get_current_groupname() -> Option { - OSUsers::empty_cache().get_current_groupname() + let gid = get_current_gid(); + get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name).unwrap()) } -/// Return the group ID for the effective user running the process. +/// Returns the group ID for the effective user running the process. pub fn get_effective_gid() -> gid_t { - OSUsers::empty_cache().get_effective_gid() + unsafe { getegid() } } -/// Return the groupname of the effective user running the process. +/// Returns the groupname of the effective user running the process. pub fn get_effective_groupname() -> Option { - OSUsers::empty_cache().get_effective_groupname() + let gid = get_effective_gid(); + get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name).unwrap()) } -/// Set current user for the running process, requires root priviledges. -pub fn set_current_uid(uid: uid_t) -> Result<(), io::Error> { +/// Sets current user for the running process, requires root priviledges. +pub fn set_current_uid(uid: uid_t) -> IOResult<()> { match unsafe { setuid(uid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } /// Set current group for the running process, requires root priviledges. -pub fn set_current_gid(gid: gid_t) -> Result<(), io::Error> { +pub fn set_current_gid(gid: gid_t) -> IOResult<()> { match unsafe { setgid(gid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } /// Set effective user for the running process, requires root priviledges. -pub fn set_effective_uid(uid: uid_t) -> Result<(), io::Error> { +pub fn set_effective_uid(uid: uid_t) -> IOResult<()> { match unsafe { seteuid(uid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } /// Set effective user for the running process, requires root priviledges. -pub fn set_effective_gid(gid: gid_t) -> Result<(), io::Error> { +pub fn set_effective_gid(gid: gid_t) -> IOResult<()> { match unsafe { setegid(gid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } /// Atomically set current and effective user for the running process, requires root priviledges. -pub fn set_both_uid(ruid: uid_t, euid: uid_t) -> Result<(), io::Error> { +pub fn set_both_uid(ruid: uid_t, euid: uid_t) -> IOResult<()> { match unsafe { setreuid(ruid, euid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } /// Atomically set current and effective group for the running process, requires root priviledges. -pub fn set_both_gid(rgid: gid_t, egid: gid_t) -> Result<(), io::Error> { +pub fn set_both_gid(rgid: gid_t, egid: gid_t) -> IOResult<()> { match unsafe { setregid(rgid, egid) } { 0 => Ok(()), - -1 => Err(io::Error::last_os_error()), + -1 => Err(IOError::last_os_error()), _ => unreachable!() } } @@ -659,7 +478,7 @@ impl Drop for SwitchUserGuard { /// Use with care! Possible security issues can happen, as Rust doesn't /// guarantee running the destructor! If in doubt run `drop()` method /// on the guard value manually! -pub fn switch_user_group(uid: uid_t, gid: gid_t) -> Result { +pub fn switch_user_group(uid: uid_t, gid: gid_t) -> Result { let current_state = SwitchUserGuard { uid: get_effective_uid(), gid: get_effective_gid(), @@ -670,79 +489,74 @@ pub fn switch_user_group(uid: uid_t, gid: gid_t) -> Result(users: &mut U) { //! println!("Current user: {:?}", users.get_current_username()); //! } //! //! let mut users = MockUsers::with_current_uid(1001); -//! users.add_user(User { uid: 1001, name: "fred".to_string(), primary_group: 101 , home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string()}); +//! users.add_user(User { uid: 1001, name: Arc::new("fred".to_string()), primary_group: 101 , home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string()}); //! print_current_username(&mut users); //! //! let mut actual_users = OSUsers::empty_cache(); //! print_current_username(&mut actual_users); //! ``` -pub use super::{Users, User, Group}; +pub use super::{Users, Groups, User, Group}; use std::collections::HashMap; +use std::sync::Arc; use libc::{uid_t, gid_t}; /// A mocking users object that you can add your own users and groups to. pub struct MockUsers { - users: HashMap, - groups: HashMap, + users: HashMap>, + groups: HashMap>, uid: uid_t, } @@ -73,134 +77,137 @@ impl MockUsers { } /// Add a user to the users table. - pub fn add_user(&mut self, user: User) -> Option { - self.users.insert(user.uid, user) + pub fn add_user(&mut self, user: User) -> Option> { + self.users.insert(user.uid, Arc::new(user)) } /// Add a group to the groups table. - pub fn add_group(&mut self, group: Group) -> Option { - self.groups.insert(group.gid, group) + pub fn add_group(&mut self, group: Group) -> Option> { + self.groups.insert(group.gid, Arc::new(group)) } } impl Users for MockUsers { - fn get_user_by_uid(&mut self, uid: uid_t) -> Option { + fn get_user_by_uid(&self, uid: uid_t) -> Option> { self.users.get(&uid).cloned() } - fn get_user_by_name(&mut self, username: &str) -> Option { - self.users.values().find(|u| u.name == username).cloned() + fn get_user_by_name(&self, username: &str) -> Option> { + self.users.values().find(|u| &*u.name == username).cloned() } - fn get_group_by_gid(&mut self, gid: gid_t) -> Option { + fn get_current_uid(&self) -> uid_t { + self.uid + } + + fn get_current_username(&self) -> Option> { + self.users.get(&self.uid).map(|u| u.name.clone()) + } + + fn get_effective_uid(&self) -> uid_t { + self.uid + } + + fn get_effective_username(&self) -> Option> { + self.users.get(&self.uid).map(|u| u.name.clone()) + } +} + +impl Groups for MockUsers { + fn get_group_by_gid(&self, gid: gid_t) -> Option> { self.groups.get(&gid).cloned() } - fn get_group_by_name(&mut self, group_name: &str) -> Option { - self.groups.values().find(|g| g.name == group_name).cloned() + fn get_group_by_name(&self, group_name: &str) -> Option> { + self.groups.values().find(|g| &*g.name == group_name).cloned() } - fn get_current_uid(&mut self) -> uid_t { + fn get_current_gid(&self) -> uid_t { self.uid } - fn get_current_username(&mut self) -> Option { - self.users.get(&self.uid).map(|u| u.name.clone()) - } - - fn get_current_gid(&mut self) -> uid_t { - self.uid - } - - fn get_current_groupname(&mut self) -> Option { + fn get_current_groupname(&self) -> Option> { self.groups.get(&self.uid).map(|u| u.name.clone()) } - fn get_effective_uid(&mut self) -> uid_t { + fn get_effective_gid(&self) -> uid_t { self.uid } - fn get_effective_username(&mut self) -> Option { - self.users.get(&self.uid).map(|u| u.name.clone()) - } - - fn get_effective_gid(&mut self) -> uid_t { - self.uid - } - - fn get_effective_groupname(&mut self) -> Option { + fn get_effective_groupname(&self) -> Option> { self.groups.get(&self.uid).map(|u| u.name.clone()) } } #[cfg(test)] mod test { - use super::{Users, User, Group, MockUsers}; + use super::{Users, Groups, User, Group, MockUsers}; + use std::sync::Arc; #[test] fn current_username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1337, name: "fred".to_string(), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); - assert_eq!(Some("fred".to_string()), users.get_current_username()) + users.add_user(User { uid: 1337, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); + assert_eq!(Some(Arc::new("fred".into())), users.get_current_username()) } #[test] fn no_current_username() { - let mut users = MockUsers::with_current_uid(1337); + let users = MockUsers::with_current_uid(1337); assert_eq!(None, users.get_current_username()) } #[test] fn uid() { let mut users = MockUsers::with_current_uid(0); - users.add_user(User { uid: 1337, name: "fred".to_string(), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); - assert_eq!(Some("fred".to_string()), users.get_user_by_uid(1337).map(|u| u.name)) + users.add_user(User { uid: 1337, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); + assert_eq!(Some(Arc::new("fred".into())), users.get_user_by_uid(1337).map(|u| u.name.clone())) } #[test] fn username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1440, name: "fred".to_string(), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); + users.add_user(User { uid: 1440, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); assert_eq!(Some(1440), users.get_user_by_name("fred").map(|u| u.uid)) } #[test] fn no_username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1440, name: "fred".to_string(), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); + users.add_user(User { uid: 1440, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); assert_eq!(None, users.get_user_by_name("criminy").map(|u| u.uid)) } #[test] fn no_uid() { - let mut users = MockUsers::with_current_uid(0); - assert_eq!(None, users.get_user_by_uid(1337).map(|u| u.name)) + let users = MockUsers::with_current_uid(0); + assert_eq!(None, users.get_user_by_uid(1337).map(|u| u.name.clone())) } #[test] fn gid() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: "fred".to_string(), members: vec![], }); - assert_eq!(Some("fred".to_string()), users.get_group_by_gid(1337).map(|g| g.name)) + users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); + assert_eq!(Some(Arc::new("fred".into())), users.get_group_by_gid(1337).map(|g| g.name.clone())) } #[test] fn group_name() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: "fred".to_string(), members: vec![], }); + users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); assert_eq!(Some(1337), users.get_group_by_name("fred").map(|g| g.gid)) } #[test] fn no_group_name() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: "fred".to_string(), members: vec![], }); + users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); assert_eq!(None, users.get_group_by_name("santa").map(|g| g.gid)) } #[test] fn no_gid() { - let mut users = MockUsers::with_current_uid(0); - assert_eq!(None, users.get_group_by_gid(1337).map(|g| g.name)) + let users = MockUsers::with_current_uid(0); + assert_eq!(None, users.get_group_by_gid(1337).map(|g| g.name.clone())) } } diff --git a/src/os.rs b/src/os.rs new file mode 100644 index 0000000..dd92427 --- /dev/null +++ b/src/os.rs @@ -0,0 +1,314 @@ +//! A cache for users and groups provided by the OS. +//! +//! ## Caching, multiple threads, and mutability +//! +//! The `OSUsers` type is caught between a rock and a hard place when it comes +//! to providing references to users and groups. +//! +//! Instead of returning a fresh `User` struct each time, for example, it will +//! return a reference to the version it currently has in its cache. So you can +//! ask for User #501 twice, and you’ll get a reference to the same value both +//! time. Its methods are *idempotent* -- calling one multiple times has the +//! same effect as calling one once. +//! +//! This works fine in theory, but in practice, the cache has to update its own +//! state somehow: it contains several `HashMap`s that hold the result of user +//! and group lookups. Rust provides mutability in two ways: +//! +//! 1. Have its methods take `&mut self`, instead of `&self`, allowing the +//! internal maps to be mutated (“inherited mutability”) +//! 2. Wrap the internal maps in a `RefCell`, allowing them to be modified +//! (“interior mutability”). +//! +//! Unfortunately, Rust is also very protective of references to a mutable +//! value. In this case, switching to `&mut self` would only allow for one user +//! to be read at a time! +//! +//! ``norun +//! let mut cache = OSUsers::empty_cache(); +//! let uid = cache.get_current_uid(); // OK... +//! let user = cache.get_user_by_uid(uid).unwrap() // OK... +//! let group = cache.get_group_by_gid(user.primary_group); // No! +//! ``` +//! +//! When we get the `user`, it returns an optional reference (which we unwrap) +//! to the user’s entry in the cache. This is a reference to something contained +//! in a mutable value. Then, when we want to get the user’s primary group, it +//! will return *another* reference to the same mutable value. This is something +//! that Rust explicitly disallows! +//! +//! The compiler wasn’t on our side with Option 1, so let’s try Option 2: +//! changing the methods back to `&self` instead of `&mut self`, and using +//! `RefCell`s internally. However, Rust is smarter than this, and knows that +//! we’re just trying the same trick as earlier. A simplified implementation of +//! a user cache lookup would look something like this: +//! +//! ``norun +//! fn get_user_by_uid(&self, uid: uid_t) -> Option<&User> { +//! let users = self.users.borrow_mut(); +//! users.get(uid) +//! } +//! ``` +//! +//! Rust won’t allow us to return a reference like this because the `Ref` of the +//! `RefCell` just gets dropped at the end of the method, meaning that our +//! reference does not live long enough. +//! +//! So instead of doing any of that, we use `Arc` everywhere in order to get +//! around all the lifetime restrictions. Returning reference-counted users and +//! groups mean that we don’t have to worry about further uses of the cache, as +//! the values themselves don’t count as being stored *in* the cache anymore. So +//! it can be queried multiple times or go out of scope and the values it +//! produces are not affected. + +use libc::{uid_t, gid_t}; +use std::borrow::ToOwned; +use std::cell::{Cell, RefCell}; +use std::collections::hash_map::Entry::{Occupied, Vacant}; +use std::collections::HashMap; +use std::sync::Arc; + +use super::{User, Groups, Group, Users}; + + +/// A producer of user and group instances that caches every result. +pub struct OSUsers { + users: BiMap, + groups: BiMap, + + uid: Cell>, + gid: Cell>, + euid: Cell>, + egid: Cell>, +} + +/// A kinda-bi-directional HashMap that associates keys to values, and then +/// strings back to keys. It doesn’t go the full route and offer +/// *values*-to-keys lookup, because we only want to search based on +/// usernames and group names. There wouldn’t be much point offering a “User +/// to uid” map, as the uid is present in the user struct! +struct BiMap { + forward: RefCell< HashMap>> >, + backward: RefCell< HashMap, Option> >, +} + +// Default has to be impl'd manually here, because there's no +// Default impl on User or Group, even though those types aren't +// needed to produce a default instance of any HashMaps... + +impl Default for OSUsers { + fn default() -> OSUsers { + OSUsers { + users: BiMap { + forward: RefCell::new(HashMap::new()), + backward: RefCell::new(HashMap::new()), + }, + + groups: BiMap { + forward: RefCell::new(HashMap::new()), + backward: RefCell::new(HashMap::new()), + }, + + uid: Cell::new(None), + gid: Cell::new(None), + euid: Cell::new(None), + egid: Cell::new(None), + } + } +} + +impl OSUsers { + + /// Create a new empty cache. + pub fn empty_cache() -> OSUsers { + OSUsers::default() + } +} + +impl Users for OSUsers { + fn get_user_by_uid(&self, uid: uid_t) -> Option> { + let mut users_forward = self.users.forward.borrow_mut(); + + match users_forward.entry(uid) { + Vacant(entry) => { + match super::get_user_by_uid(uid) { + Some(user) => { + let newsername = user.name.clone(); + let mut users_backward = self.users.backward.borrow_mut(); + users_backward.insert(newsername, Some(uid)); + + let user_arc = Arc::new(user); + entry.insert(Some(user_arc.clone())); + Some(user_arc) + }, + None => { + entry.insert(None); + None + } + } + }, + Occupied(entry) => entry.get().clone(), + } + } + + fn get_user_by_name(&self, username: &str) -> Option> { + let mut users_backward = self.users.backward.borrow_mut(); + + // to_owned() could change here: + // https://github.com/rust-lang/rfcs/blob/master/text/0509-collections-reform-part-2.md#alternatives-to-toowned-on-entries + match users_backward.entry(Arc::new(username.to_owned())) { + Vacant(entry) => { + match super::get_user_by_name(username) { + Some(user) => { + let uid = user.uid; + let user_arc = Arc::new(user); + + let mut users_forward = self.users.forward.borrow_mut(); + users_forward.insert(uid, Some(user_arc.clone())); + entry.insert(Some(uid)); + + Some(user_arc) + }, + None => { + entry.insert(None); + None + } + } + }, + Occupied(entry) => match *entry.get() { + Some(uid) => { + let users_forward = self.users.forward.borrow_mut(); + users_forward[&uid].clone() + } + None => None, + } + } + } + + fn get_current_uid(&self) -> uid_t { + match self.uid.get() { + Some(uid) => uid, + None => { + let uid = super::get_current_uid(); + self.uid.set(Some(uid)); + uid + } + } + } + + fn get_current_username(&self) -> Option> { + let uid = self.get_current_uid(); + self.get_user_by_uid(uid).map(|u| u.name.clone()) + } + + fn get_effective_uid(&self) -> uid_t { + match self.euid.get() { + Some(uid) => uid, + None => { + let uid = super::get_effective_uid(); + self.euid.set(Some(uid)); + uid + } + } + } + + fn get_effective_username(&self) -> Option> { + let uid = self.get_effective_uid(); + self.get_user_by_uid(uid).map(|u| u.name.clone()) + } +} + +impl Groups for OSUsers { + fn get_group_by_gid(&self, gid: gid_t) -> Option> { + let mut groups_forward = self.groups.forward.borrow_mut(); + + match groups_forward.entry(gid) { + Vacant(entry) => { + let group = super::get_group_by_gid(gid); + match group { + Some(group) => { + let new_group_name = group.name.clone(); + let mut groups_backward = self.groups.backward.borrow_mut(); + groups_backward.insert(new_group_name, Some(gid)); + + let group_arc = Arc::new(group); + entry.insert(Some(group_arc.clone())); + Some(group_arc) + }, + None => { + entry.insert(None); + None + } + } + }, + Occupied(entry) => entry.get().clone(), + } + } + + fn get_group_by_name(&self, group_name: &str) -> Option> { + let mut groups_backward = self.groups.backward.borrow_mut(); + + // to_owned() could change here: + // https://github.com/rust-lang/rfcs/blob/master/text/0509-collections-reform-part-2.md#alternatives-to-toowned-on-entries + match groups_backward.entry(Arc::new(group_name.to_owned())) { + Vacant(entry) => { + let user = super::get_group_by_name(group_name); + match user { + Some(group) => { + let group_arc = Arc::new(group.clone()); + let gid = group.gid; + + let mut groups_forward = self.groups.forward.borrow_mut(); + groups_forward.insert(gid, Some(group_arc.clone())); + entry.insert(Some(gid)); + + Some(group_arc) + }, + None => { + entry.insert(None); + None + } + } + }, + Occupied(entry) => match *entry.get() { + Some(gid) => { + let groups_forward = self.groups.forward.borrow_mut(); + groups_forward[&gid].as_ref().cloned() + } + None => None, + } + } + } + + fn get_current_gid(&self) -> gid_t { + match self.gid.get() { + Some(gid) => gid, + None => { + let gid = super::get_current_gid(); + self.gid.set(Some(gid)); + gid + } + } + } + + fn get_current_groupname(&self) -> Option> { + let gid = self.get_current_gid(); + self.get_group_by_gid(gid).map(|g| g.name.clone()) + } + + fn get_effective_gid(&self) -> gid_t { + match self.egid.get() { + Some(gid) => gid, + None => { + let gid = super::get_effective_gid(); + self.egid.set(Some(gid)); + gid + } + } + } + + fn get_effective_groupname(&self) -> Option> { + let gid = self.get_effective_gid(); + self.get_group_by_gid(gid).map(|g| g.name.clone()) + } +}