From 772cf3e5360debc677275d86b31030b7d9102f64 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Tue, 21 Jan 2020 16:35:12 -0500 Subject: [PATCH 01/12] Trait objects without explicit 'dyn' deprecated. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1ed89e7..1b895a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -328,7 +328,7 @@ impl Error for LookupError { fn description(&self) -> &str { "lookup error" } - fn cause(&self) -> Option<&Error> { + fn cause(&self) -> Option<&dyn Error> { Some(&self.cause) } } From ecf2d17c2f4295f6ca6439d69405e1b75cb60506 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Tue, 21 Jan 2020 23:39:26 -0500 Subject: [PATCH 02/12] Support for `~otheruser` expansion: linux+bsd Uses libc to add support for tilde expansion involving other users (e.g. `~otheruser/directory`) in the case of linux and the BSDs. --- Cargo.toml | 2 + src/home_dir/macos.rs | 18 ++++++ src/home_dir/mod.rs | 128 ++++++++++++++++++++++++++++++++++++++++ src/home_dir/nix.rs | 116 ++++++++++++++++++++++++++++++++++++ src/home_dir/other.rs | 22 +++++++ src/home_dir/windows.rs | 22 +++++++ src/lib.rs | 98 ++++++++++++++++++++++++++---- 7 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 src/home_dir/macos.rs create mode 100644 src/home_dir/mod.rs create mode 100644 src/home_dir/nix.rs create mode 100644 src/home_dir/other.rs create mode 100644 src/home_dir/windows.rs diff --git a/Cargo.toml b/Cargo.toml index 52ef3fc..c7c7473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,5 @@ keywords = ["strings", "shell", "variables"] [dependencies] dirs = "2.0" +[target.'cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd",))'.dependencies] +libc = "0.2" diff --git a/src/home_dir/macos.rs b/src/home_dir/macos.rs new file mode 100644 index 0000000..a9ea5c0 --- /dev/null +++ b/src/home_dir/macos.rs @@ -0,0 +1,18 @@ +use std::path::PathBuf; + +use super::HomeDirError; + +/// Returns the home directory of: +/// * the current user if `user` is `None` or an empty string, or +/// * the provided user if `user` is anything else. +pub(crate) fn home_dir(user: Option<&str>) -> Result { + let user = match user { + None | Some("") => { + // When user is `None` or an empty string, let the `dirs` crate + // do the work. + dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) + } + Some(user) => user, + }; + todo!() +} diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs new file mode 100644 index 0000000..abe916c --- /dev/null +++ b/src/home_dir/mod.rs @@ -0,0 +1,128 @@ +/* + * ******************* + * OS-specific modules + * ******************* +*/ + +// linux and bsd +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", +))] +mod nix; + +// macos +#[cfg(target_os = "macos")] +mod macos; + +// windows +#[cfg(target_os = "windows")] +mod windows; + +// all others +#[cfg(not(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "windows", +)))] +mod other; + +/* + * ******************* + * OS-specific exports + * ******************* +*/ + +// linux and unix (excluding macos) +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", +))] +pub(crate) use self::nix::home_dir; + +// macos +#[cfg(target_os = "macos")] +pub(crate) use self::macos::home_dir; + +// windows +#[cfg(target_os = "windows")] +pub(crate) use self::windows::home_dir; + +// all others +#[cfg(not(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "windows", +)))] +pub(crate) use self::other::home_dir; + +/* + * ******************* + * Common + * ******************* +*/ + +use std::fmt; + +/// Internal error type used for debugging. Not exposed publicly. +#[derive(Debug)] +pub(crate) struct HomeDirError(HomeDirErrorKind); + +impl HomeDirError { + #[allow(unused)] + fn libc_error(msg: Option<&str>) -> Self { + let kind = HomeDirErrorKind::Libc(msg.map(|s| s.to_string())); + Self(kind) + } + + #[allow(unused)] + fn not_found(user: Option<&str>) -> Self { + let kind = HomeDirErrorKind::NotFound(user.map(|s| s.to_string())); + Self(kind) + } + + #[allow(unused)] + fn unimplemented() -> Self { + let kind = HomeDirErrorKind::Unimplemented; + Self(kind) + } +} + +impl fmt::Display for HomeDirError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::HomeDirErrorKind::*; + match self.0 { + Libc(Some(ref msg)) => write!(f, "libc error while looking up home directory: {}.", msg), + Libc(None) => write!(f, "libc error while looking up home directory."), + NotFound(Some(ref user)) => { + write!(f, "Unable to find home directory for user {}.", user) + } + NotFound(None) => write!(f, "Unable to find home directory for current user."), + Unimplemented => write!(f, "Identifying the home directory of a user other than the current user is not yet implemented for this platform."), + } + } +} + +impl std::error::Error for HomeDirError {} + +#[derive(Debug)] +pub(crate) enum HomeDirErrorKind { + Libc(Option), + NotFound(Option), + Unimplemented, +} + diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs new file mode 100644 index 0000000..7306119 --- /dev/null +++ b/src/home_dir/nix.rs @@ -0,0 +1,116 @@ +use std::ffi::{CStr, OsString}; +use std::mem; +use std::os::unix::ffi::OsStringExt; +use std::path::PathBuf; + +use super::HomeDirError; + +/// Returns the home directory of: +/// * the current user if `user` is `None` or an empty string, or +/// * the provided user if `user` is anything else. +pub(crate) fn home_dir(user: Option<&str>) -> Result { + let user = match user { + None | Some("") => { + // When user is `None` or an empty string, let the `dirs` crate + // do the work. + return dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)); + } + Some(user) => user, + }; + + // Turn user into a "c string," i.e. a null terminated vector of bytes, + // whose lifetime will outlive the calls to the libc functions below. + let user_c = { + let mut bytes = user.as_bytes().iter().cloned().collect::>(); + bytes.push(b'\0'); + bytes + }; + + // Ask libc for the user's home directory with `getpwnam_r`. + const BUFLEN: usize = 1024; + let mut buf: [u8; BUFLEN] = [0; BUFLEN]; + // Safety: All `getpwnam_r` requires is that `pwd` has enough space to hold a passwd struct. + let mut pwd: libc::passwd = unsafe { mem::zeroed() }; + // Safety: All `getpwnam_r` requires is that `result` has enough space to hold a passwd struct. + let mut result: libc::passwd = unsafe { mem::zeroed() }; + let mut result_ptr = &mut result as *mut libc::passwd; + let result_ptr_ptr = &mut result_ptr as *mut *mut libc::passwd; + let err = unsafe { + libc::getpwnam_r( + /* const char* name */ user_c.as_ptr() as *const libc::c_char, + /* struct passwd* pwd */ &mut pwd as *mut libc::passwd, + /* char* buf */ buf.as_mut_ptr() as *mut libc::c_char, + /* size_t buflen */ BUFLEN as libc::size_t, + /* struct passwd** result */ result_ptr_ptr, + ) + }; + + // Check libc's error, if any. + if err != 0 { + // If the error is due to insufficient buffer space, that's our fault. + // Panic with the following message. + if err == libc::ERANGE { + panic!("libc error while looking up home directory: Insufficient buffer space supplied. This is an implementation error and should be unreachable. If you see this message, please file an issue at https://github.com/rust-lang/rust.") + } + + // Otherwise, ask libc for the message associated with this error. + #[rustfmt::skip] + let err = unsafe { + libc::strerror_r( + /* int errnum */ err, + /* char* buf */ buf.as_mut_ptr() as *mut libc::c_char, + /* size_t buflen */ BUFLEN, + ) + }; + + // If the call to `strerror_r` itself fails, return an unknown libc error. + if err != 0 { + return Err(HomeDirError::libc_error(None)); + } + + // Otherwise, convert the error message into a rust &str and return it. + let msg = CStr::from_bytes_with_nul(&buf[..]) + .map_err(|_| HomeDirError::libc_error(None))? + .to_string_lossy(); + return Err(HomeDirError::libc_error(Some(&msg))); + } + + // If `results_ptr_ptr` is null, it means libc was unable to locate the home directory. + // Return a not found error. + if result_ptr_ptr.is_null() { + return Err(HomeDirError::not_found(Some(user))); + } + + // Libc should ensure that the `pw.pwdir` pointer is always valid; if + // for some reason it doesn't, return an unknown libc error. + if pwd.pw_dir.is_null() { + return Err(HomeDirError::libc_error(None)); + } + + // We have found the user's home directory. Convert the `pwd.pw_dir` pointer, which + // we know is valid, into a rust `PathBuf` and return it. + let home_dir = { + // Safety: Safe because we check above that `pwd.pw_dir` is valid. + let bytes = unsafe { CStr::from_ptr(pwd.pw_dir) }.to_bytes().to_vec(); + let os_string = OsString::from_vec(bytes); + PathBuf::from(os_string) + }; + Ok(home_dir) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + #[test] + fn test_home_dir() { + // Test for the current user. + let _ = home_dir(None).unwrap(); + // Test for a different user. `root` is the only user account + // I can think of that should be on all *nix systems. + let path = home_dir(Some("root")).unwrap(); + assert_eq!(path, Path::new("/root")); + } +} diff --git a/src/home_dir/other.rs b/src/home_dir/other.rs new file mode 100644 index 0000000..7a77536 --- /dev/null +++ b/src/home_dir/other.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +use super::HomeDirError; + +/// Returns the home directory of the current user if `user` is `None` or +/// an empty string. In the future, may return the home directory of the +/// provided user if `user` is anything else, but that is not yet implemented +/// for non-unix platforms. +pub(crate) fn home_dir(user: Option<&str>) -> Result { + match user { + None | Some("") => { + // When user is `None` or an empty string, let the `dirs` crate + // do the work. + dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) + } + Some(user) => { + // Finding the home directory of a user other than the current + // user is not yet implemented on windows. + Err(HomeDirError::Unimplemented), + } + } +} diff --git a/src/home_dir/windows.rs b/src/home_dir/windows.rs new file mode 100644 index 0000000..7a77536 --- /dev/null +++ b/src/home_dir/windows.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +use super::HomeDirError; + +/// Returns the home directory of the current user if `user` is `None` or +/// an empty string. In the future, may return the home directory of the +/// provided user if `user` is anything else, but that is not yet implemented +/// for non-unix platforms. +pub(crate) fn home_dir(user: Option<&str>) -> Result { + match user { + None | Some("") => { + // When user is `None` or an empty string, let the `dirs` crate + // do the work. + dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) + } + Some(user) => { + // Finding the home directory of a user other than the current + // user is not yet implemented on windows. + Err(HomeDirError::Unimplemented), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1b895a6..421edf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,8 @@ extern crate dirs; +mod home_dir; + use std::borrow::Cow; use std::env::VarError; use std::error::Error; @@ -253,17 +255,14 @@ where /// Performs both tilde and environment expansions in the default system context. /// -/// This function delegates to `full_with_context()`, using the default system sources for both -/// home directory and environment, namely `dirs::home_dir()` and `std::env::var()`. +/// This function delegates to using the default system sources for both +/// home directory and environment. It first calls `env` and then calls `tilde`. /// /// Note that variable lookup of unknown variables will fail with an error instead of, for example, /// replacing the unknown variable with an empty string. The author thinks that this behavior is /// more useful than the other ones. If you need to change it, use `full_with_context()` or /// `full_with_context_no_errors()` with an appropriate context function instead. /// -/// This function behaves exactly like `full_with_context()` in regard to tilde-containing -/// variables in the beginning of the input string. -/// /// # Examples /// /// ``` @@ -292,12 +291,29 @@ where /// }) /// ); /// ``` -#[inline] pub fn full(input: &SI) -> Result, LookupError> where SI: AsRef, { - full_with_context(input, dirs::home_dir, |s| std::env::var(s).map(Some)) + env(input).map(|r| match r { + // variable expansion did not modify the original string, so we can apply tilde expansion + // directly + Cow::Borrowed(s) => tilde(s), + Cow::Owned(s) => { + // if the original string does not start with a tilde but the processed one does, + // then the tilde is contained in one of variables and should not be expanded + if !input.as_ref().starts_with("~") && s.starts_with("~") { + // return as is + s.into() + } else { + if let Cow::Owned(s) = tilde(&s) { + s.into() + } else { + s.into() + } + } + } + }) } /// Represents a variable lookup error. @@ -641,7 +657,9 @@ where input_str.into() } } else { - // we cannot handle `~otheruser/` paths yet + // `FnOnce() -> Option

` api does not allow for handling `~otheruser/` paths here. + // Would need to make breaking change to `FnOnce(username: Option<&str>) -> Option

` + // in order to handle it. input_str.into() } } else { @@ -650,10 +668,10 @@ where } } -/// Performs the tilde expansion using the default system context. +/// Performs tilde expansion using the default system context. /// -/// This function delegates to `tilde_with_context()`, using the default system source of home -/// directory path, namely `dirs::home_dir()` function. +/// Note: Unlike the `tilde_with_context` function, this function **does** support +/// expansions such as ~anotheruser/directory. /// /// # Examples /// @@ -669,12 +687,49 @@ where /// format!("{}/some/dir", hds) /// ); /// ``` -#[inline] pub fn tilde(input: &SI) -> Cow where SI: AsRef, { - tilde_with_context(input, dirs::home_dir) + let input_str = input.as_ref(); + if !input_str.starts_with("~") { + return input_str.into(); + } + let input_after_tilde = &input_str[1..]; + + // Pull out the username from `input_after_tilde`, if any. In the case of `~`, the username + // should be `None`. In the case of `~otheruser`, the username should be `Some("otheruser")`. + let (username, remainder) = + if input_after_tilde.is_empty() || input_after_tilde.starts_with("/") { + // '~' case... + (None, input_after_tilde) + } else { + // `~otheruser` case... + + // We would like to pull out the username as a `&str`. We know the start of + // username is byte index 0 of `input_after_tilde`. Let's find the byte index + // of `input_after_tilde` where username ends ... in a way that is careful to + // account for any utf8 byte boundary issues. + let byte_index = input_after_tilde + // Get an iterator over the byte indices and characters of `input_after_tilde` + .char_indices() + // Find the byte index of of the first `/` + .find(|(_byte_index, c)| *c == '/') + // Throw away the character because we don't need it (we know it's `/`) + .map(|(byte_index, _c)| byte_index) + // If we did not find a `/` that means the end of the username + // is the end of `input_after_tilde`. + .unwrap_or_else(|| input_after_tilde.as_bytes().len()); + // Using `byte_index`, split `input_after_tilde` into + // the username and the remainder of the path. + let (username, remainder) = input_after_tilde.split_at(byte_index); + (Some(username), remainder) + }; + + match crate::home_dir::home_dir(username) { + Ok(hd) => format!("{}{}", hd.display(), remainder).into(), + Err(_) => input_str.into(), + } } #[cfg(test)] @@ -716,6 +771,23 @@ mod tilde_tests { None => assert_eq!(tilde("~/something"), "~/something"), } } + + #[test] + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", + ))] + fn test_tilde_otheruser() { + // Note: For this test to work, the system needs to have a root user + // whose home directory is located at `/root`, which is the case for + // most, but perhaps not all, *nix systems. If your system does not meet + // this requirement, disable this test by annotating it with `#[ignore]` + assert_eq!(tilde("~root"), "/root"); + assert_eq!(tilde("~root/something"), "/root/something"); + } } #[cfg(test)] From 51f9cb3f300141f527774495ed6cc30beb02cd3e Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Tue, 21 Jan 2020 23:44:31 -0500 Subject: [PATCH 03/12] 2018 edition --- Cargo.toml | 1 + src/lib.rs | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c7c7473..412b465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/netvl/shellexpand" documentation = "http://netvl.github.io/shellexpand/" readme = "Readme.md" keywords = ["strings", "shell", "variables"] +edition = "2018" [dependencies] dirs = "2.0" diff --git a/src/lib.rs b/src/lib.rs index 421edf2..6685e23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,8 +83,6 @@ //! The above example also demonstrates the flexibility of context function signatures: the context //! function may return anything which can be `AsRef`ed into a string slice. -extern crate dirs; - mod home_dir; use std::borrow::Cow; @@ -266,7 +264,6 @@ where /// # Examples /// /// ``` -/// extern crate dirs; /// use std::env; /// /// env::set_var("A", "a value"); @@ -676,8 +673,6 @@ where /// # Examples /// /// ``` -/// extern crate dirs; -/// /// let hds = dirs::home_dir() /// .map(|p| p.display().to_string()) /// .unwrap_or_else(|| "~".to_owned()); From b4def2c6e97f307655e676cc4f5270cbb01deb45 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Tue, 21 Jan 2020 23:48:11 -0500 Subject: [PATCH 04/12] typo --- src/home_dir/nix.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs index 7306119..5a0f5aa 100644 --- a/src/home_dir/nix.rs +++ b/src/home_dir/nix.rs @@ -50,7 +50,7 @@ pub(crate) fn home_dir(user: Option<&str>) -> Result { // If the error is due to insufficient buffer space, that's our fault. // Panic with the following message. if err == libc::ERANGE { - panic!("libc error while looking up home directory: Insufficient buffer space supplied. This is an implementation error and should be unreachable. If you see this message, please file an issue at https://github.com/rust-lang/rust.") + panic!("libc error while looking up home directory: Insufficient buffer space supplied. This is an implementation error and should be unreachable. If you see this message, please file an issue at https://github.com/netvl/shellexpand.") } // Otherwise, ask libc for the message associated with this error. From 2bd8402289d6b9b52fba1419b27f85a24c15b54f Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Wed, 22 Jan 2020 00:32:36 -0500 Subject: [PATCH 05/12] update for macos --- Cargo.toml | 2 +- src/home_dir/macos.rs | 18 ------------------ src/home_dir/mod.rs | 15 ++++----------- src/home_dir/nix.rs | 7 ++++++- src/lib.rs | 7 +++++++ 5 files changed, 18 insertions(+), 31 deletions(-) delete mode 100644 src/home_dir/macos.rs diff --git a/Cargo.toml b/Cargo.toml index 412b465..fb62b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,5 @@ edition = "2018" [dependencies] dirs = "2.0" -[target.'cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd",))'.dependencies] +[target.'cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd", target_os = "macos"))'.dependencies] libc = "0.2" diff --git a/src/home_dir/macos.rs b/src/home_dir/macos.rs deleted file mode 100644 index a9ea5c0..0000000 --- a/src/home_dir/macos.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::path::PathBuf; - -use super::HomeDirError; - -/// Returns the home directory of: -/// * the current user if `user` is `None` or an empty string, or -/// * the provided user if `user` is anything else. -pub(crate) fn home_dir(user: Option<&str>) -> Result { - let user = match user { - None | Some("") => { - // When user is `None` or an empty string, let the `dirs` crate - // do the work. - dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) - } - Some(user) => user, - }; - todo!() -} diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs index abe916c..85a5680 100644 --- a/src/home_dir/mod.rs +++ b/src/home_dir/mod.rs @@ -4,20 +4,17 @@ * ******************* */ -// linux and bsd +// linux, macos, and bsd #[cfg(any( target_os = "dragonfly", target_os = "freebsd", target_os = "linux", + target_os = "macos", target_os = "netbsd", target_os = "openbsd", ))] mod nix; -// macos -#[cfg(target_os = "macos")] -mod macos; - // windows #[cfg(target_os = "windows")] mod windows; @@ -40,20 +37,17 @@ mod other; * ******************* */ -// linux and unix (excluding macos) +// linux, macos, and bsd #[cfg(any( target_os = "dragonfly", target_os = "freebsd", target_os = "linux", + target_os = "macos", target_os = "netbsd", target_os = "openbsd", ))] pub(crate) use self::nix::home_dir; -// macos -#[cfg(target_os = "macos")] -pub(crate) use self::macos::home_dir; - // windows #[cfg(target_os = "windows")] pub(crate) use self::windows::home_dir; @@ -125,4 +119,3 @@ pub(crate) enum HomeDirErrorKind { NotFound(Option), Unimplemented, } - diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs index 5a0f5aa..3f950d4 100644 --- a/src/home_dir/nix.rs +++ b/src/home_dir/nix.rs @@ -111,6 +111,11 @@ mod tests { // Test for a different user. `root` is the only user account // I can think of that should be on all *nix systems. let path = home_dir(Some("root")).unwrap(); - assert_eq!(path, Path::new("/root")); + let expected = if cfg!(target_os = "macos") { + Path::new("/var/root") + } else { + Path::new("/root") + }; + assert_eq!(path, expected); } } diff --git a/src/lib.rs b/src/lib.rs index 6685e23..a12b740 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -783,6 +783,13 @@ mod tilde_tests { assert_eq!(tilde("~root"), "/root"); assert_eq!(tilde("~root/something"), "/root/something"); } + + #[test] + #[cfg(target_os = "macos")] + fn test_tilde_otheruser() { + assert_eq!(tilde("~root"), "/var/root"); + assert_eq!(tilde("~root/something"), "/var/root/something"); + } } #[cfg(test)] From 33279fb94a7bbb67d0a9837b5de7640a382a0873 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Wed, 22 Jan 2020 00:36:28 -0500 Subject: [PATCH 06/12] Update comment --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a12b740..0292673 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -668,7 +668,7 @@ where /// Performs tilde expansion using the default system context. /// /// Note: Unlike the `tilde_with_context` function, this function **does** support -/// expansions such as ~anotheruser/directory. +/// expansions such as ~anotheruser/directory (on linux, macos and the BSDs). /// /// # Examples /// From 7eb9ca6b9c8db67b7d3f0d356d9f232439e009f3 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 26 Mar 2020 01:21:27 -0700 Subject: [PATCH 07/12] Removed try_lookup macro in favor of question mark and combinators Also updated the std::error::Error implementation --- src/lib.rs | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0292673..c1bc4d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -327,6 +327,12 @@ pub struct LookupError { pub cause: E, } +impl LookupError { + fn for_var(var_name: impl Into) -> impl FnOnce(E) -> LookupError { + |e| LookupError { var_name: var_name.into(), cause: e } + } +} + impl fmt::Display for LookupError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( @@ -337,29 +343,12 @@ impl fmt::Display for LookupError { } } -impl Error for LookupError { - fn description(&self) -> &str { - "lookup error" - } - fn cause(&self) -> Option<&dyn Error> { +impl Error for LookupError { + fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.cause) } } -macro_rules! try_lookup { - ($name:expr, $e:expr) => { - match $e { - Ok(s) => s, - Err(e) => { - return Err(LookupError { - var_name: $name.into(), - cause: e, - }) - } - } - }; -} - fn is_valid_var_name_char(c: char) -> bool { c.is_alphanumeric() || c == '_' } @@ -462,7 +451,7 @@ where match input_str.find('}') { Some(closing_brace_idx) => { let var_name = &input_str[2..closing_brace_idx]; - match try_lookup!(var_name, context(var_name)) { + match context(var_name).map_err(LookupError::for_var(var_name))? { Some(var_value) => { result.push_str(var_value.as_ref()); input_str = &input_str[closing_brace_idx + 1..]; @@ -487,7 +476,7 @@ where .unwrap_or(input_str.len() - 2); let var_name = &input_str[1..end_idx]; - match try_lookup!(var_name, context(var_name)) { + match context(var_name).map_err(LookupError::for_var(var_name))? { Some(var_value) => { result.push_str(var_value.as_ref()); input_str = &input_str[end_idx..]; From f3dc143ac80b6881f5a1846e73d5ba989debd518 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 26 Mar 2020 01:23:23 -0700 Subject: [PATCH 08/12] Simplified conditional compilation config Also added a feature flag to explicitly toggle the other users home dir support --- Cargo.toml | 8 +++-- src/home_dir/mod.rs | 69 +++++++++-------------------------------- src/home_dir/nix.rs | 8 ++--- src/home_dir/other.rs | 4 +-- src/home_dir/windows.rs | 22 ------------- 5 files changed, 25 insertions(+), 86 deletions(-) delete mode 100644 src/home_dir/windows.rs diff --git a/Cargo.toml b/Cargo.toml index fb62b92..aef5ef4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,12 @@ readme = "Readme.md" keywords = ["strings", "shell", "variables"] edition = "2018" +[features] +default = ["home-dir"] +home-dir = ["libc"] + [dependencies] dirs = "2.0" -[target.'cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd", target_os = "macos"))'.dependencies] -libc = "0.2" +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2", optional = true } diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs index 85a5680..d6b7ecc 100644 --- a/src/home_dir/mod.rs +++ b/src/home_dir/mod.rs @@ -4,31 +4,12 @@ * ******************* */ -// linux, macos, and bsd -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", -))] +// unixes +#[cfg(all(unix, feature = "home-dir"))] mod nix; -// windows -#[cfg(target_os = "windows")] -mod windows; - -// all others -#[cfg(not(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "windows", -)))] +// others +#[cfg(not(all(unix, feature = "home-dir")))] mod other; /* @@ -37,31 +18,12 @@ mod other; * ******************* */ -// linux, macos, and bsd -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", -))] +// unixes +#[cfg(all(unix, feature = "home-dir"))] pub(crate) use self::nix::home_dir; -// windows -#[cfg(target_os = "windows")] -pub(crate) use self::windows::home_dir; - // all others -#[cfg(not(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "windows", -)))] +#[cfg(not(all(unix, feature = "home-dir")))] pub(crate) use self::other::home_dir; /* @@ -70,6 +32,7 @@ pub(crate) use self::other::home_dir; * ******************* */ +use std::error::Error; use std::fmt; /// Internal error type used for debugging. Not exposed publicly. @@ -99,19 +62,17 @@ impl HomeDirError { impl fmt::Display for HomeDirError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::HomeDirErrorKind::*; - match self.0 { - Libc(Some(ref msg)) => write!(f, "libc error while looking up home directory: {}.", msg), - Libc(None) => write!(f, "libc error while looking up home directory."), - NotFound(Some(ref user)) => { - write!(f, "Unable to find home directory for user {}.", user) - } - NotFound(None) => write!(f, "Unable to find home directory for current user."), - Unimplemented => write!(f, "Identifying the home directory of a user other than the current user is not yet implemented for this platform."), + match &self.0 { + Libc(Some(msg)) => write!(f, "libc error while looking up home directory: {}", msg), + Libc(None) => write!(f, "libc error while looking up home directory"), + NotFound(Some(user)) => write!(f, "Unable to find home directory for user {}", user), + NotFound(None) => write!(f, "Unable to find home directory for current user"), + Unimplemented => write!(f, "Identifying the home directory of a user other than the current user is not yet implemented for this platform"), } } } -impl std::error::Error for HomeDirError {} +impl Error for HomeDirError {} #[derive(Debug)] pub(crate) enum HomeDirErrorKind { diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs index 3f950d4..c2d1ce6 100644 --- a/src/home_dir/nix.rs +++ b/src/home_dir/nix.rs @@ -1,4 +1,4 @@ -use std::ffi::{CStr, OsString}; +use std::ffi::{CStr, CString, OsString}; use std::mem; use std::os::unix::ffi::OsStringExt; use std::path::PathBuf; @@ -20,11 +20,7 @@ pub(crate) fn home_dir(user: Option<&str>) -> Result { // Turn user into a "c string," i.e. a null terminated vector of bytes, // whose lifetime will outlive the calls to the libc functions below. - let user_c = { - let mut bytes = user.as_bytes().iter().cloned().collect::>(); - bytes.push(b'\0'); - bytes - }; + let user_c = CString::new(user).expect("User name contains an unexpected zero byte"); // Ask libc for the user's home directory with `getpwnam_r`. const BUFLEN: usize = 1024; diff --git a/src/home_dir/other.rs b/src/home_dir/other.rs index 7a77536..4b9a59d 100644 --- a/src/home_dir/other.rs +++ b/src/home_dir/other.rs @@ -5,7 +5,7 @@ use super::HomeDirError; /// Returns the home directory of the current user if `user` is `None` or /// an empty string. In the future, may return the home directory of the /// provided user if `user` is anything else, but that is not yet implemented -/// for non-unix platforms. +/// for non-unix platforms and some unix platforms too. pub(crate) fn home_dir(user: Option<&str>) -> Result { match user { None | Some("") => { @@ -16,7 +16,7 @@ pub(crate) fn home_dir(user: Option<&str>) -> Result { Some(user) => { // Finding the home directory of a user other than the current // user is not yet implemented on windows. - Err(HomeDirError::Unimplemented), + Err(HomeDirError::Unimplemented) } } } diff --git a/src/home_dir/windows.rs b/src/home_dir/windows.rs deleted file mode 100644 index 7a77536..0000000 --- a/src/home_dir/windows.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::path::PathBuf; - -use super::HomeDirError; - -/// Returns the home directory of the current user if `user` is `None` or -/// an empty string. In the future, may return the home directory of the -/// provided user if `user` is anything else, but that is not yet implemented -/// for non-unix platforms. -pub(crate) fn home_dir(user: Option<&str>) -> Result { - match user { - None | Some("") => { - // When user is `None` or an empty string, let the `dirs` crate - // do the work. - dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) - } - Some(user) => { - // Finding the home directory of a user other than the current - // user is not yet implemented on windows. - Err(HomeDirError::Unimplemented), - } - } -} From 2cea6aef87c0071f29e8833755b1dc57ec6e3e83 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Sun, 29 Mar 2020 21:30:03 -0400 Subject: [PATCH 09/12] Clean up nix; add redox; make libc optional * Clean up nix to use thread-local buffers in order to avoid allocations * Add redox module * Make libc optional dependency to enable unixes without required libc functions to still be able to compile * Add a check.py script in order to ensure that code compiles on several different OSs --- .gitignore | 1 + Cargo.toml | 16 +++- scripts/check.py | 86 ++++++++++++++++++ src/home_dir/mod.rs | 74 +++++++++------ src/home_dir/nix.rs | 196 +++++++++++++++++++++++++++------------- src/home_dir/other.rs | 22 ++--- src/home_dir/redox.rs | 28 ++++++ src/home_dir/windows.rs | 11 +++ 8 files changed, 323 insertions(+), 111 deletions(-) create mode 100755 scripts/check.py create mode 100644 src/home_dir/redox.rs create mode 100644 src/home_dir/windows.rs diff --git a/.gitignore b/.gitignore index 4bf519a..758e41f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock .*.sw? .idea *.iml +.mypy_cache diff --git a/Cargo.toml b/Cargo.toml index 645d0c0..10337c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,18 @@ readme = "Readme.md" keywords = ["strings", "shell", "variables"] edition = "2018" -[features] -default = ["home-dir"] -home-dir = ["libc"] - [dependencies] dirs = "2.0" -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] libc = { version = "0.2", optional = true } + +[target.'cfg(target_os = "redox")'.dependencies] +redox_users = "0.3" + +[target.'cfg(windows)'.dependencies] +winapi = "0.3" + +[features] +default = ["libc"] + diff --git a/scripts/check.py b/scripts/check.py new file mode 100755 index 0000000..971aa10 --- /dev/null +++ b/scripts/check.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +import subprocess +import sys +from typing import Dict, Set + +# { os: (channel, [triple...]) } +OSS = { + "android": ( + "stable", + ["aarch64-linux-android", "armv7-linux-androideabi", "i686-linux-android",], + ), + "freebsd": ("stable", ["x86_64-unknown-freebsd"]), + "linux": ("stable", ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]), + "macos": ("stable", ["x86_64-apple-darwin"]), + "redox": ("nightly", ["x86_64-unknown-redox"]), + "netbsd": ("stable", ["x86_64-unknown-netbsd"]), + "windows": ("stable", ["x86_64-pc-windows-gnu", "x86_64-pc-windows-msvc"]), +} + +if sys.platform == "darwin": + OSS["ios"] = ("stable", ["aarch64-apple-ios", "x86_64-apple-ios",]) + + +def blue(s: str) -> str: + return "\033[94m" + s + "\033[0m" + + +def get_installed_targets() -> Dict[str, Set[str]]: + installed: Dict[str, Set[str]] = {} + for channel in ["stable", "nightly"]: + installed[channel] = set() + command = f"rustup +{channel} target list" + completed = subprocess.run(command.split(), capture_output=True) + if completed.returncode != 0: + print(completed.stderr) + sys.exit(1) + for line in completed.stdout.splitlines(): + split = line.split() + if len(split) == 2 and split[1].decode("utf-8") == "(installed)": + triple = split[0].decode("utf-8") + installed[channel].add(triple) + return installed + + +def run_command(command: str): + print(blue(command)) + completed = subprocess.run(command.split()) + if completed.returncode != 0: + sys.exit(1) + + +def run_cargo_check(channel: str, triple: str): + command = f"cargo +{channel} check --target={triple}" + run_command(command) + + +def run_rustup_target_install(channel: str, triple: str): + command = f"rustup +{channel} target install {triple}" + run_command(command) + + +def main(): + installed = get_installed_targets() + if len(sys.argv) == 1: + for (channel, ts) in OSS.values(): + for t in ts: + if t not in installed[channel]: + run_rustup_target_install(channel, t) + run_cargo_check(channel, t) + else: + for os in sys.argv[1:]: + value = OSS.get(os) + if value is None: + available = str(list(OSS.keys())) + print(f"Unrecognized OS '{os}'. Must be one of: {available}.") + sys.exit(1) + (channel, ts) = value + for t in ts: + if t not in installed[channel]: + run_rustup_target_install(channel, t) + run_cargo_check(channel, t) + + +if __name__ == "__main__": + main() diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs index d6b7ecc..ffc87c4 100644 --- a/src/home_dir/mod.rs +++ b/src/home_dir/mod.rs @@ -4,12 +4,24 @@ * ******************* */ -// unixes -#[cfg(all(unix, feature = "home-dir"))] +// nix (with libc feature) +#[cfg(all(unix, not(target_os = "redox"), feature = "libc"))] mod nix; -// others -#[cfg(not(all(unix, feature = "home-dir")))] +// redox +#[cfg(target_os = "redox")] +mod redox; + +// windows +#[cfg(windows)] +mod windows; + +// all others +#[cfg(not(any( + all(unix, not(target_os = "redox"), feature = "libc"), + target_os = "redox", + windows, +)))] mod other; /* @@ -18,12 +30,24 @@ mod other; * ******************* */ -// unixes -#[cfg(all(unix, feature = "home-dir"))] +// nix (with libc feature enabled) +#[cfg(all(unix, not(target_os = "redox"), feature = "libc"))] pub(crate) use self::nix::home_dir; +// redox +#[cfg(target_os = "redox")] +pub(crate) use self::redox::home_dir; + +// windows +#[cfg(windows)] +pub(crate) use self::windows::home_dir; + // all others -#[cfg(not(all(unix, feature = "home-dir")))] +#[cfg(not(any( + all(unix, not(target_os = "redox"), feature = "libc"), + target_os = "redox", + windows, +)))] pub(crate) use self::other::home_dir; /* @@ -39,44 +63,34 @@ use std::fmt; #[derive(Debug)] pub(crate) struct HomeDirError(HomeDirErrorKind); -impl HomeDirError { - #[allow(unused)] - fn libc_error(msg: Option<&str>) -> Self { - let kind = HomeDirErrorKind::Libc(msg.map(|s| s.to_string())); - Self(kind) - } - - #[allow(unused)] - fn not_found(user: Option<&str>) -> Self { - let kind = HomeDirErrorKind::NotFound(user.map(|s| s.to_string())); - Self(kind) - } - - #[allow(unused)] - fn unimplemented() -> Self { - let kind = HomeDirErrorKind::Unimplemented; - Self(kind) - } -} - impl fmt::Display for HomeDirError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::HomeDirErrorKind::*; match &self.0 { - Libc(Some(msg)) => write!(f, "libc error while looking up home directory: {}", msg), - Libc(None) => write!(f, "libc error while looking up home directory"), NotFound(Some(user)) => write!(f, "Unable to find home directory for user {}", user), NotFound(None) => write!(f, "Unable to find home directory for current user"), + OS(Some(msg)) => write!(f, "libc error while looking up home directory: {}", msg), + OS(None) => write!(f, "libc error while looking up home directory"), Unimplemented => write!(f, "Identifying the home directory of a user other than the current user is not yet implemented for this platform"), } } } +impl HomeDirError { + fn not_found(user: Option<&str>) -> Self { + let kind = HomeDirErrorKind::NotFound(user.map(|s| s.to_string())); + Self(kind) + } +} + impl Error for HomeDirError {} #[derive(Debug)] pub(crate) enum HomeDirErrorKind { - Libc(Option), + #[allow(unused)] NotFound(Option), + #[allow(unused)] + OS(Option), + #[allow(unused)] Unimplemented, } diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs index c2d1ce6..f853007 100644 --- a/src/home_dir/nix.rs +++ b/src/home_dir/nix.rs @@ -1,78 +1,88 @@ -use std::ffi::{CStr, CString, OsString}; +use std::borrow::BorrowMut; +use std::cell::RefCell; +use std::ffi::{CStr, OsString}; use std::mem; use std::os::unix::ffi::OsStringExt; use std::path::PathBuf; -use super::HomeDirError; +use super::{HomeDirError, HomeDirErrorKind}; + +thread_local! { + // Allocate required buffers once per thread instead of each time 'home_dir' + // is called so that users who call 'home_dir' multiple times on the same + // thread are spared uncessary allocations + // + // Note: It's prudent here to use heap-allocated buffers instead of stack- + // allocated arrays because it's quite difficult (in a cross-platform, 100% + // portable way) to ask libc for the maximum possible lengths of usernames + // and home directories. There doesn't appear to be a good POSIX standard + // for this that all *nix systems adhere to. + static BUF0: RefCell> = RefCell::new(vec![0; 1024]); + static BUF1: RefCell> = RefCell::new(vec![0; 1]); +} /// Returns the home directory of: /// * the current user if `user` is `None` or an empty string, or /// * the provided user if `user` is anything else. pub(crate) fn home_dir(user: Option<&str>) -> Result { + BUF0.with(move |buf0| { + BUF1.with(move |buf1| { + let mut buf0 = buf0.borrow_mut(); + let mut buf1 = buf1.borrow_mut(); + _home_dir(user, buf0.borrow_mut(), buf1.borrow_mut()) + }) + }) +} + +fn _home_dir( + user: Option<&str>, + buf0: &mut Vec, + buf1: &mut Vec, +) -> Result { let user = match user { None | Some("") => { - // When user is `None` or an empty string, let the `dirs` crate - // do the work. + // When user is `None` or an empty string, let the `dirs` crate do the work. return dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)); } Some(user) => user, }; - // Turn user into a "c string," i.e. a null terminated vector of bytes, - // whose lifetime will outlive the calls to the libc functions below. - let user_c = CString::new(user).expect("User name contains an unexpected zero byte"); + // Copy c-string version of user into buf0 + copy_into_buffer(user, buf0.borrow_mut()); - // Ask libc for the user's home directory with `getpwnam_r`. - const BUFLEN: usize = 1024; - let mut buf: [u8; BUFLEN] = [0; BUFLEN]; - // Safety: All `getpwnam_r` requires is that `pwd` has enough space to hold a passwd struct. + // Initialze out parameters of 'libc::getpwnam_r' let mut pwd: libc::passwd = unsafe { mem::zeroed() }; - // Safety: All `getpwnam_r` requires is that `result` has enough space to hold a passwd struct. let mut result: libc::passwd = unsafe { mem::zeroed() }; let mut result_ptr = &mut result as *mut libc::passwd; let result_ptr_ptr = &mut result_ptr as *mut *mut libc::passwd; - let err = unsafe { - libc::getpwnam_r( - /* const char* name */ user_c.as_ptr() as *const libc::c_char, - /* struct passwd* pwd */ &mut pwd as *mut libc::passwd, - /* char* buf */ buf.as_mut_ptr() as *mut libc::c_char, - /* size_t buflen */ BUFLEN as libc::size_t, - /* struct passwd** result */ result_ptr_ptr, - ) - }; - // Check libc's error, if any. - if err != 0 { - // If the error is due to insufficient buffer space, that's our fault. - // Panic with the following message. - if err == libc::ERANGE { - panic!("libc error while looking up home directory: Insufficient buffer space supplied. This is an implementation error and should be unreachable. If you see this message, please file an issue at https://github.com/netvl/shellexpand.") - } - - // Otherwise, ask libc for the message associated with this error. - #[rustfmt::skip] - let err = unsafe { - libc::strerror_r( - /* int errnum */ err, - /* char* buf */ buf.as_mut_ptr() as *mut libc::c_char, - /* size_t buflen */ BUFLEN, + loop { + // Call 'libc::getpwnam_r' to write the user's home directory into buf1 + let ret = unsafe { + libc::getpwnam_r( + /* const char* name */ buf0.as_ptr() as *const libc::c_char, + /* struct passwd* pwd */ &mut pwd as *mut libc::passwd, + /* char* buf */ buf1.as_mut_ptr() as *mut libc::c_char, + /* size_t buflen */ buf1.len() as libc::size_t, + /* struct passwd** result */ result_ptr_ptr, ) }; - - // If the call to `strerror_r` itself fails, return an unknown libc error. - if err != 0 { - return Err(HomeDirError::libc_error(None)); + match ret { + // If successful, break + 0 => break, + // If buf1 was too small to hold the user's home directory, + // double the size of buf1 and try again + libc::ERANGE => { + buf1.resize(buf1.len() * 2, 0); + continue; + } + // If unsuccessful due to any other error, return a libc error + errnum => return Err(HomeDirError::os(Some(errnum))), } - - // Otherwise, convert the error message into a rust &str and return it. - let msg = CStr::from_bytes_with_nul(&buf[..]) - .map_err(|_| HomeDirError::libc_error(None))? - .to_string_lossy(); - return Err(HomeDirError::libc_error(Some(&msg))); } - // If `results_ptr_ptr` is null, it means libc was unable to locate the home directory. - // Return a not found error. + // If `results_ptr_ptr` is null, it means libc was unable to locate the home + // directory. Return a not found error. if result_ptr_ptr.is_null() { return Err(HomeDirError::not_found(Some(user))); } @@ -80,20 +90,71 @@ pub(crate) fn home_dir(user: Option<&str>) -> Result { // Libc should ensure that the `pw.pwdir` pointer is always valid; if // for some reason it doesn't, return an unknown libc error. if pwd.pw_dir.is_null() { - return Err(HomeDirError::libc_error(None)); + return Err(HomeDirError::os(None)); } - // We have found the user's home directory. Convert the `pwd.pw_dir` pointer, which - // we know is valid, into a rust `PathBuf` and return it. + // We have found the user's home directory. Convert the `pwd.pw_dir` pointer, + // which we know is valid, into a rust `PathBuf` and return it. let home_dir = { // Safety: Safe because we check above that `pwd.pw_dir` is valid. let bytes = unsafe { CStr::from_ptr(pwd.pw_dir) }.to_bytes().to_vec(); let os_string = OsString::from_vec(bytes); PathBuf::from(os_string) }; + Ok(home_dir) } +fn copy_into_buffer(src: &str, dst: &mut Vec) { + let len = src.len(); + // Ensure dst is large enough to hold src bytes plus a NULL byte + dst.resize(len + 1, 0); + // Copy src bytes into dst + (&mut dst[..len]).copy_from_slice(src.as_bytes()); + // Add NULL byte at the end + dst[len] = b'\0'; +} + +impl HomeDirError { + /// Converts an optional errnum from C into a HomeDirError + fn os(errnum: Option) -> Self { + if errnum.is_none() { + let kind = HomeDirErrorKind::OS(None); + return Self(kind); + } + + let errnum = errnum.unwrap(); + + // Initialize a c-string buffer on the stack large enough to hold + // the error message + let mut buf = [0u8; 1024]; + + // Use `libc::strerror_r` to get the error message + #[rustfmt::skip] + let ret = unsafe { + libc::strerror_r( + /* int errnum */ errnum, + /* char* buf */ buf.as_mut_ptr() as *mut libc::c_char, + /* size_t buflen */ buf.len(), + ) + }; + + // If `libc::strerror_r` fails, return an unknown libc error. + if ret != 0 { + let kind = HomeDirErrorKind::OS(None); + return Self(kind); + } + + // Otherwise, convert the message and return it. + let kind = match CStr::from_bytes_with_nul(&buf[..]) { + Ok(msg) => HomeDirErrorKind::OS(Some(msg.to_string_lossy().into())), + Err(_) => HomeDirErrorKind::OS(None), + }; + + Self(kind) + } +} + #[cfg(test)] mod tests { use std::path::Path; @@ -102,16 +163,27 @@ mod tests { #[test] fn test_home_dir() { - // Test for the current user. - let _ = home_dir(None).unwrap(); - // Test for a different user. `root` is the only user account - // I can think of that should be on all *nix systems. - let path = home_dir(Some("root")).unwrap(); - let expected = if cfg!(target_os = "macos") { - Path::new("/var/root") - } else { - Path::new("/root") - }; - assert_eq!(path, expected); + // Spawn many threads to ensure thread-safety + const NTHREADS: usize = 100; + let mut handles = Vec::with_capacity(NTHREADS); + for _ in 0..NTHREADS { + let handle = std::thread::spawn(|| { + // Test for the current user. + let _ = home_dir(None).unwrap(); + // Test for a different user. `root` is the only user account + // I can think of that should be on all *nix systems. + let path = home_dir(Some("root")).unwrap(); + let expected = if cfg!(target_os = "macos") { + Path::new("/var/root") + } else { + Path::new("/root") + }; + assert_eq!(path, expected); + }); + handles.push(handle); + } + for handle in handles { + handle.join().unwrap(); + } } } diff --git a/src/home_dir/other.rs b/src/home_dir/other.rs index 4b9a59d..51b5f3a 100644 --- a/src/home_dir/other.rs +++ b/src/home_dir/other.rs @@ -1,22 +1,16 @@ use std::path::PathBuf; -use super::HomeDirError; +use super::{HomeDirError, HomeDirErrorKind}; /// Returns the home directory of the current user if `user` is `None` or -/// an empty string. In the future, may return the home directory of the -/// provided user if `user` is anything else, but that is not yet implemented -/// for non-unix platforms and some unix platforms too. +/// an empty string. +/// +/// In the future, may also return the home directory of the provided user if +/// `user` is anything else, but that is not currently implemented for this +/// platform. pub(crate) fn home_dir(user: Option<&str>) -> Result { match user { - None | Some("") => { - // When user is `None` or an empty string, let the `dirs` crate - // do the work. - dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)) - } - Some(user) => { - // Finding the home directory of a user other than the current - // user is not yet implemented on windows. - Err(HomeDirError::Unimplemented) - } + None | Some("") => dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)), + Some(_user) => Err(HomeDirError(HomeDirErrorKind::Unimplemented)), } } diff --git a/src/home_dir/redox.rs b/src/home_dir/redox.rs new file mode 100644 index 0000000..e53647c --- /dev/null +++ b/src/home_dir/redox.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use redox_users::All; + +use super::HomeDirError; + +/// Returns the home directory of: +/// * the current user if `user` is `None` or an empty string, or +/// * the provided user if `user` is anything else. +pub(crate) fn home_dir(user: Option<&str>) -> Result { + _home_dir(user).ok_or_else(|| HomeDirError::not_found(user)) +} + +fn _home_dir(user: Option<&str>) -> Option { + let config = redox_users::Config::default(); + let users = redox_users::AllUsers::new(config).ok()?; + match user { + None | Some("") => { + let uid = redox_users::get_uid().ok()?; + let user = users.get_by_id(uid)?; + Some(PathBuf::from(user.home.clone())) + } + Some(user) => { + let user = users.get_by_name(user)?; + Some(PathBuf::from(user.home.clone())) + } + } +} diff --git a/src/home_dir/windows.rs b/src/home_dir/windows.rs new file mode 100644 index 0000000..06cf33c --- /dev/null +++ b/src/home_dir/windows.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +use super::HomeDirError; + +/// Returns the home directory of: +/// * the current user if `user` is `None` or an empty string, +/// * the Default user if `user` is `Some("Default")`, or +/// * the provided user if `user` is anything else. +pub(crate) fn home_dir(_: Option<&str>) -> Result { + todo!() +} From 05e6a7a2ddd756c19822bb65d6bb45b9252b2368 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Sun, 29 Mar 2020 21:39:47 -0400 Subject: [PATCH 10/12] windows support --- Cargo.toml | 2 +- src/home_dir/mod.rs | 7 +- src/home_dir/windows.rs | 550 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 552 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10337c5..df01f77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ libc = { version = "0.2", optional = true } redox_users = "0.3" [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi = { version = "0.3", features = ["errhandlingapi", "handleapi", "psapi", "securitybaseapi", "userenv"] } [features] default = ["libc"] diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs index ffc87c4..afa2fa3 100644 --- a/src/home_dir/mod.rs +++ b/src/home_dir/mod.rs @@ -69,8 +69,9 @@ impl fmt::Display for HomeDirError { match &self.0 { NotFound(Some(user)) => write!(f, "Unable to find home directory for user {}", user), NotFound(None) => write!(f, "Unable to find home directory for current user"), - OS(Some(msg)) => write!(f, "libc error while looking up home directory: {}", msg), - OS(None) => write!(f, "libc error while looking up home directory"), + OS(Some(msg)) => write!(f, "OS error while looking up home directory: {}", msg), + OS(None) => write!(f, "OS error while looking up home directory"), + PermissionDenied(user) => write!(f, "Permission denied. Reading home directory of {} requires elevated priviliges.", user), Unimplemented => write!(f, "Identifying the home directory of a user other than the current user is not yet implemented for this platform"), } } @@ -92,5 +93,7 @@ pub(crate) enum HomeDirErrorKind { #[allow(unused)] OS(Option), #[allow(unused)] + PermissionDenied(String), + #[allow(unused)] Unimplemented, } diff --git a/src/home_dir/windows.rs b/src/home_dir/windows.rs index 06cf33c..eeb7e8d 100644 --- a/src/home_dir/windows.rs +++ b/src/home_dir/windows.rs @@ -1,11 +1,553 @@ +use std::borrow::BorrowMut; +use std::cell::{RefCell, RefMut}; +use std::ffi::OsString; use std::path::PathBuf; -use super::HomeDirError; +use winapi::shared::minwindef::DWORD; +use winapi::um::winnt::WCHAR; + +use super::{HomeDirError, HomeDirErrorKind}; + +thread_local! { + // Allocate required buffers once per thread instead of each time 'home_dir' + // is called so that users who call 'home_dir' multiple times on the same + // thread are spared uncessary allocations + static BUF_DWORD: RefCell> = RefCell::new(vec![0; 1024]); + static BUF_U8: RefCell> = RefCell::new(vec![0; 1024]); + static BUF_WCHAR: RefCell> = RefCell::new(vec![0; 1024]); +} /// Returns the home directory of: /// * the current user if `user` is `None` or an empty string, -/// * the Default user if `user` is `Some("Default")`, or +/// * the Default user is `user` is `Some("Default")`, or /// * the provided user if `user` is anything else. -pub(crate) fn home_dir(_: Option<&str>) -> Result { - todo!() +/// +/// On Windows, querying the home directory of any user other than the +/// current user or the Default user requires: +/// * Elevated priviliges (i.e. `Run as Administrator`), +/// * That the other user is logged in +#[rustfmt::skip] +pub(crate) fn home_dir(user: Option<&str>) -> Result { + BUF_DWORD.with(move |buf_dword| { + BUF_U8.with(move |buf_u8| { + BUF_WCHAR.with(move |buf_wchar| { + let mut buf_dword: RefMut> = buf_dword.borrow_mut(); + let mut buf_u8: RefMut> = buf_u8.borrow_mut(); + let mut buf_wchar: RefMut> = buf_wchar.borrow_mut(); + + let buf_dword: &mut Vec = buf_dword.borrow_mut(); + let buf_u8: &mut Vec = buf_u8.borrow_mut(); + let buf_wchar: &mut Vec = buf_wchar.borrow_mut(); + + match user { + Some("") | None => get_profile_directory(None, buf_dword, buf_u8, buf_wchar), + Some("Default") => sys::get_default_user_profile_directory(buf_wchar), + Some(user) => get_profile_directory(Some(user), buf_dword, buf_u8, buf_wchar), + } + }) + }) + }) +} + +/// Returns the profile directory of the provided user. +fn get_profile_directory( + user: Option<&str>, + buf_dword: &mut Vec, + buf_u8: &mut Vec, + buf_wchar: &mut Vec, +) -> Result { + let mut current_process = sys::get_current_process()?; + let current_user = get_user(&mut current_process, buf_u8, buf_wchar)?; + let mut current_token = sys::open_process_token(&mut current_process, buf_wchar)?; + + let user = match user { + None => { + let path = sys::get_user_profile_directory(&mut current_token, buf_wchar)?; + return Ok(path); + } + Some(user) if user == current_user => { + let path = sys::get_user_profile_directory(&mut current_token, buf_wchar)?; + return Ok(path); + } + Some(user) => user, + }; + + // If we reach here, we're looking for the home directory of another user. + // On Windows unfortunatley this requires: + // + // 1) That we have elevated priviliges, and + // 2) The other user is logged in + // + // because we need one of their token handles and the only way to get one + // of those is through a handle to a running process that they are the user + // of, which we cannot read unless we have elevated priviliges. + + // If the user doesn't have elevated priviliges, return a PermissionDenied error. + let has_elevated_priviliges = + sys::get_token_information_token_elevation(&mut current_token, buf_wchar)?; + if !has_elevated_priviliges { + return Err(HomeDirError::permission_denied(user)); + } + + // Now we fill `buf_dword` with a list of the pids of all running processes. + sys::enum_processes(buf_dword, buf_wchar)?; + + // For each pid, we first try to get the username of the process' user. + // If that username matches the username we're looking for, we then try to + // get that user's home directory. If this doesn't work for any pid, we + // return a not found error. + + fn for_each_pid( + pid: DWORD, + user: &str, + buf_u8: &mut Vec, + buf_wchar: &mut Vec, + ) -> Option { + if pid == 0 { + return None; + } + let mut process = sys::open_process(pid, buf_wchar).ok()?; + let mut token = sys::open_process_token(&mut process, buf_wchar).ok()?; + let sid = sys::get_token_information_token_user(&mut token, buf_u8, buf_wchar).ok()?; + let s = sys::lookup_account_sid(sid, buf_wchar).ok()?; + if &s == user { + let path = sys::get_user_profile_directory(&mut token, buf_wchar).ok()?; + return Some(path); + } + None + } + + for &pid in buf_dword.iter() { + match for_each_pid(pid, user, buf_u8, buf_wchar) { + Some(path) => return Ok(path), + None => continue, + } + } + Err(HomeDirError::not_found(Some(user))) +} + +/// Returns the username of the user associated with the provided process. +fn get_user( + process: &mut dyn handles::Process, + buf_u8: &mut Vec, + buf_wchar: &mut Vec, +) -> Result { + let mut token = sys::open_process_token(process, buf_wchar)?; + let sid = sys::get_token_information_token_user(&mut token, buf_u8, buf_wchar)?; + let user = sys::lookup_account_sid(sid, buf_wchar)?; + Ok(user) +} + +impl HomeDirError { + fn os(buf_wchar: &mut Vec) -> Self { + let errnum = sys::get_last_error(); + let msg = sys::format_message(errnum, buf_wchar); + Self(HomeDirErrorKind::OS(Some(msg))) + } + + fn os_from_errnum(errnum: DWORD, buf_wchar: &mut Vec) -> Self { + let msg = sys::format_message(errnum, buf_wchar); + Self(HomeDirErrorKind::OS(Some(msg))) + } + + fn os_from_str(msg: S) -> Self + where + S: Into, + { + Self(HomeDirErrorKind::OS(Some(msg.into()))) + } + + fn permission_denied(user: S) -> Self + where + S: Into, + { + Self(HomeDirErrorKind::PermissionDenied(user.into())) + } +} + +/// Safe wrappers around raw winapi C functions +mod sys { + use std::ffi::OsString; + use std::mem; + use std::os::windows::ffi::OsStringExt; + use std::path::PathBuf; + use std::ptr::NonNull; + + use winapi::ctypes::c_void; + use winapi::shared::minwindef::DWORD; + use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; + use winapi::um::errhandlingapi::GetLastError; + use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcess, OpenProcessToken}; + use winapi::um::psapi::EnumProcesses; + use winapi::um::securitybaseapi::GetTokenInformation; + use winapi::um::userenv::{GetDefaultUserProfileDirectoryW, GetUserProfileDirectoryW}; + use winapi::um::winbase::{FormatMessageW, LookupAccountSidW, FORMAT_MESSAGE_FROM_SYSTEM}; + use winapi::um::winnt::{ + TokenElevation, TokenUser, PROCESS_QUERY_INFORMATION, SID_NAME_USE, TOKEN_ELEVATION, + TOKEN_QUERY, TOKEN_USER, + }; + use winapi::um::winnt::{HANDLE, WCHAR}; + + use super::handles::{NonNullDrop, Process, ProcessCurrent, ProcessOther, Sid, Token}; + use super::HomeDirError; + + /// Fills `buf_dword` with the process identifier for each process object in the system. + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocesses + pub(crate) fn enum_processes( + buf_dword: &mut Vec, + buf_wchar: &mut Vec, + ) -> Result<(), HomeDirError> { + loop { + let nbytes = (buf_dword.len() * mem::size_of::()) as DWORD; + let mut nbytes_filled: DWORD = 0; + let ret = unsafe { + EnumProcesses( + /* DWORD* lpidProcess */ buf_dword.as_mut_ptr(), + /* DWORD cb */ nbytes, + /* LPDWORD lpcbNeeded */ &mut nbytes_filled as *mut DWORD, + ) + }; + if ret == 0 { + return Err(HomeDirError::os(buf_wchar)); + } + if nbytes == nbytes_filled { + buf_dword.resize(buf_dword.len() * 2, 0); + continue; + } + let len = nbytes_filled as usize / mem::size_of::(); + buf_dword.resize(len, 0); + break; + } + Ok(()) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessagew + pub(crate) fn format_message(errnum: DWORD, buf_wchar: &mut Vec) -> String { + let mut len; + loop { + len = unsafe { + FormatMessageW( + /* DWORD dwFlags */ FORMAT_MESSAGE_FROM_SYSTEM, + /* LPCVOID lpSource */ std::ptr::null(), + /* DWORD dwMessageId */ errnum, + /* DWORD dwLanguageId */ 0, + /* LPWSTR lpBuffer */ buf_wchar.as_mut_ptr(), + /* DWORD nSize */ buf_wchar.len() as DWORD, + /* va_list* Arguments */ std::ptr::null_mut(), + ) + }; + if len != 0 { + break; + } + buf_wchar.resize(buf_wchar.len() * 2, 0); + } + OsString::from_wide(&buf_wchar[..len as usize]) + .to_string_lossy() + .trim() + .to_string() + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocess + pub(crate) fn get_current_process() -> Result { + let handle = unsafe { GetCurrentProcess() }; + Ok(NonNull::new(handle).ok_or_else(|| { + HomeDirError::os_from_str("GetCurrentProcess unexpectedly returned a null pointer.") + })?) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getdefaultuserprofiledirectoryw + pub(crate) fn get_default_user_profile_directory( + buf_wchar: &mut Vec, + ) -> Result { + let mut len: DWORD = buf_wchar.len() as DWORD; + loop { + let ret = unsafe { + GetDefaultUserProfileDirectoryW( + /* LPWSTR lpProfileDir */ buf_wchar.as_mut_ptr() as *mut WCHAR, + /* LPDWORD lpcchSize */ &mut len as *mut DWORD, + ) + }; + if ret == 0 { + match unsafe { GetLastError() } { + ERROR_INSUFFICIENT_BUFFER => { + buf_wchar.resize(len as usize, 0); + continue; + } + errnum => return Err(HomeDirError::os_from_errnum(errnum, buf_wchar)), + } + } + break; + } + let slice = &buf_wchar[..len as usize - 1]; + let path = PathBuf::from(OsString::from_wide(slice)); + Ok(path) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror + pub(crate) fn get_last_error() -> DWORD { + unsafe { GetLastError() } + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation + pub(crate) fn get_token_information_token_elevation( + token: &mut Token, + buf_wchar: &mut Vec, + ) -> Result { + let mut elevation = unsafe { mem::zeroed::() }; + let mut nbytes = mem::size_of::() as DWORD; + let ret = unsafe { + GetTokenInformation( + token.as_ptr(), + TokenElevation, + &mut elevation as *mut TOKEN_ELEVATION as *mut c_void, + mem::size_of::() as DWORD, + &mut nbytes as *mut DWORD, + ) + }; + if ret == 0 { + return Err(HomeDirError::os(buf_wchar)); + } + let is_elevated = if elevation.TokenIsElevated == 0 { + false + } else { + true + }; + Ok(is_elevated) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation + pub(crate) fn get_token_information_token_user( + token: &mut Token, + buf_u8: &mut Vec, + buf_wchar: &mut Vec, + ) -> Result { + let mut nbytes: DWORD = 0; + loop { + #[rustfmt::skip] + let ret = unsafe { + GetTokenInformation( + /* HANDLE TokenHandle */ token.as_ptr(), + /* TOKEN_INFORMATION_CLASS TokenInformationClass */ TokenUser, + /* LPVOID TokenInformation */ buf_u8.as_mut_ptr() as *mut c_void, + /* DWORD TokenInformationLength */ buf_u8.len() as DWORD, + /* PDWORD ReturnLength */ &mut nbytes as *mut DWORD, + ) + }; + if ret == 0 { + match get_last_error() { + ERROR_INSUFFICIENT_BUFFER => { + buf_u8.resize(buf_u8.len() * 2, 0); + continue; + } + errnum => return Err(HomeDirError::os_from_errnum(errnum, buf_wchar)), + } + } + break; + } + let token_user_ptr: *const TOKEN_USER = unsafe { mem::transmute(buf_u8.as_ptr()) }; + if token_user_ptr.is_null() { + return Err(HomeDirError::os_from_str( + "GetTokenInformation returned an invalid pointer.", + )); + } + let sid_ptr = unsafe { *token_user_ptr }.User.Sid; + let sid = NonNull::new(sid_ptr).ok_or_else(|| { + HomeDirError::os_from_str("GetTokenInformation return an invalid pointer.") + })?; + Ok(sid) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getuserprofiledirectoryw + pub(crate) fn get_user_profile_directory( + token: &mut Token, + buf_wchar: &mut Vec, + ) -> Result { + let mut len = buf_wchar.len() as DWORD; + loop { + let ret = unsafe { + GetUserProfileDirectoryW( + /* HANDLE hToken */ token.as_ptr(), + /* LPWSTR lpProfileDir */ buf_wchar.as_mut_ptr() as *mut WCHAR, + /* LPDWORD lpcchSize */ &mut len as *mut DWORD, + ) + }; + if ret == 0 { + match unsafe { GetLastError() } { + ERROR_INSUFFICIENT_BUFFER => { + buf_wchar.resize(len as usize, 0); + continue; + } + errnum => { + return Err(HomeDirError::os_from_errnum(errnum, buf_wchar)); + } + } + } + break; + } + + let slice = &buf_wchar[..len as usize - 1]; + let path = PathBuf::from(OsString::from_wide(slice)); + Ok(path) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountsidw + pub(crate) fn lookup_account_sid( + mut sid: Sid, + buf_wchar: &mut Vec, + ) -> Result { + let mut buf_wchar_len: DWORD = buf_wchar.len() as DWORD; + + let mut buf_other: [WCHAR; 1024] = [0; 1024]; + let mut buf_other_len: DWORD = 1024; + + let mut sid_name_use: SID_NAME_USE = unsafe { mem::zeroed() }; + loop { + #[rustfmt::skip] + let ret = unsafe { + LookupAccountSidW( + /* LPCWSTR lpSystemName */ std::ptr::null_mut(), + /* PSID Sid */ sid.as_mut(), + /* LPWSTR Name */ buf_wchar.as_mut_ptr(), + /* LPDWORD cchName */ &mut buf_wchar_len as *mut DWORD, + /* LPWSTR ReferencedDomainName */ buf_other.as_mut_ptr(), + /* LPDWORD cchReferencedDomainName */ &mut buf_other_len as *mut DWORD, + /* PSID_NAME_USE peUse */ &mut sid_name_use as *mut SID_NAME_USE, + ) + }; + if ret == 0 { + match unsafe { GetLastError() } { + ERROR_INSUFFICIENT_BUFFER => { + buf_wchar.resize(buf_wchar_len as usize, 0); + continue; + } + errnum => { + return Err(HomeDirError::os_from_errnum(errnum, buf_wchar)); + } + } + } + break; + } + let len = match buf_wchar.iter().position(|&w| w == 0) { + Some(len) => len, + None => { + return Err(HomeDirError::os_from_str( + "LookupAccountSid unexpectedly return c-string without a nul terminator.", + )) + } + }; + let s = OsString::from_wide(&buf_wchar[..len]); + Ok(s) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess + pub(crate) fn open_process( + pid: DWORD, + buf_wchar: &mut Vec, + ) -> Result { + let process_handle = unsafe { + OpenProcess( + /* DWORD dwDesiredAccess */ PROCESS_QUERY_INFORMATION, + /* BOOL bInheritHandle */ 0, + /* DWORD dwProcessId */ pid, + ) + }; + Ok(NonNullDrop::from( + NonNull::new(process_handle).ok_or_else(|| HomeDirError::os(buf_wchar))?, + )) + } + + /// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken + pub(crate) fn open_process_token<'a>( + process: &'a mut dyn Process, + buf_wchar: &mut Vec, + ) -> Result, HomeDirError> { + let mut token_handle = unsafe { mem::zeroed::() }; + let ret = unsafe { + OpenProcessToken( + /* HANDLE ProcessHandle */ process.as_ptr(), + /* DWORD DesiredAccess */ TOKEN_QUERY, + /* PHANDLE TokenHandle */ &mut token_handle as *mut HANDLE, + ) + }; + if ret == 0 { + return Err(HomeDirError::os(buf_wchar)); + } + let ptr = NonNullDrop::from(NonNull::new(token_handle).ok_or_else(|| { + HomeDirError::os_from_str("OpenProcessHandle unexpectedly returned a null pointer.") + })?); + Ok(Token { ptr, process }) + } +} + +/// Safe wrappers for various winapi "HANDLE"s (void pointers) +pub(crate) mod handles { + use std::ops::{Deref, DerefMut}; + use std::ptr::NonNull; + + use winapi::ctypes::c_void; + use winapi::um::handleapi::CloseHandle; + + // Handles to either the current process or a SID do not need to be closed; + // so we can simply represent them with std::ptr::NonNull + pub(crate) type ProcessCurrent = NonNull; + pub(crate) type Sid = NonNull; + + // Handles to other process needs to be closed; so we represent them with + // a custom type (`NonNullDrop`, see below) that closes the handle on drop. + pub(crate) type ProcessOther = NonNullDrop; + + #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] + pub(crate) struct NonNullDrop(NonNull); + + impl From> for NonNullDrop { + fn from(ptr: NonNull) -> Self { + Self(ptr) + } + } + + impl Drop for NonNullDrop { + fn drop(&mut self) { + unsafe { CloseHandle(self.0.as_ptr() as *mut c_void) }; + } + } + + // Trait that enables functions to accept either a ProcessCurrent or a ProcessOther + pub(crate) trait Process { + fn as_ptr(&mut self) -> *mut c_void; + } + + impl Process for NonNull { + fn as_ptr(&mut self) -> *mut c_void { + Self::as_ptr(*self) + } + } + + impl Process for NonNullDrop { + fn as_ptr(&mut self) -> *mut c_void { + self.0.as_ptr() + } + } + + // Wrapper for a token handle, which includes a reference to the process + // it came from, as the process needs to outlive it (i.e. not be closed + // before the token is closed) + pub(crate) struct Token<'a> { + pub(crate) ptr: NonNullDrop, + #[allow(unused)] + pub(crate) process: &'a dyn Process, + } + + impl<'a> Deref for Token<'a> { + type Target = NonNullDrop; + fn deref(&self) -> &Self::Target { + &self.ptr + } + } + + impl<'a> DerefMut for Token<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } + } } From 14177b4a0fc5353e77365e74e26e6de252f8f390 Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Sun, 29 Mar 2020 22:06:54 -0400 Subject: [PATCH 11/12] eliminate unneeded dirs dependency --- Cargo.toml | 12 ++++++++---- scripts/check.py | 11 +++++++++++ src/home_dir/nix.rs | 26 +++++++++++++++----------- src/home_dir/other.rs | 21 +++++++++++---------- src/lib.rs | 4 ++-- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df01f77..bc5176b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,22 @@ readme = "Readme.md" keywords = ["strings", "shell", "variables"] edition = "2018" -[dependencies] -dirs = "2.0" - +# nix [target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] libc = { version = "0.2", optional = true } +# redox [target.'cfg(target_os = "redox")'.dependencies] redox_users = "0.3" +# windows [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["errhandlingapi", "handleapi", "psapi", "securitybaseapi", "userenv"] } +winapi = { version = "0.3", features = ["errhandlingapi", "handleapi", "processthreadsapi", "psapi", "securitybaseapi", "userenv", "winbase", "winerror"] } + +[dev-dependencies] +dirs = "2.0" [features] default = ["libc"] + diff --git a/scripts/check.py b/scripts/check.py index 971aa10..0545a02 100755 --- a/scripts/check.py +++ b/scripts/check.py @@ -68,6 +68,11 @@ def main(): if t not in installed[channel]: run_rustup_target_install(channel, t) run_cargo_check(channel, t) + # check linux without libc + linux_triples = OSS["linux"][1] + for triple in linux_triples: + command = f"cargo check --target={triple} --no-default-features" + run_command(command) else: for os in sys.argv[1:]: value = OSS.get(os) @@ -80,6 +85,12 @@ def main(): if t not in installed[channel]: run_rustup_target_install(channel, t) run_cargo_check(channel, t) + if "linux" in sys.argv[1]: + # check linux without libc + linux_triples = OSS["linux"][1] + for triple in linux_triples: + command = f"cargo check --target={triple} --no-default-features" + run_command(command) if __name__ == "__main__": diff --git a/src/home_dir/nix.rs b/src/home_dir/nix.rs index f853007..f31322c 100644 --- a/src/home_dir/nix.rs +++ b/src/home_dir/nix.rs @@ -1,9 +1,9 @@ use std::borrow::BorrowMut; use std::cell::RefCell; use std::ffi::{CStr, OsString}; -use std::mem; use std::os::unix::ffi::OsStringExt; use std::path::PathBuf; +use std::{env, mem}; use super::{HomeDirError, HomeDirErrorKind}; @@ -25,6 +25,13 @@ thread_local! { /// * the current user if `user` is `None` or an empty string, or /// * the provided user if `user` is anything else. pub(crate) fn home_dir(user: Option<&str>) -> Result { + if user.is_none() || user == Some("") { + if let Some(o) = env::var_os("HOME") { + if !o.is_empty() { + return Ok(PathBuf::from(o)); + } + } + } BUF0.with(move |buf0| { BUF1.with(move |buf1| { let mut buf0 = buf0.borrow_mut(); @@ -39,17 +46,14 @@ fn _home_dir( buf0: &mut Vec, buf1: &mut Vec, ) -> Result { - let user = match user { - None | Some("") => { - // When user is `None` or an empty string, let the `dirs` crate do the work. - return dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)); + let user_ptr = match user { + None | Some("") => std::ptr::null(), + Some(user) => { + copy_into_buffer(user, buf0.borrow_mut()); + buf0.as_ptr() as *const libc::c_char } - Some(user) => user, }; - // Copy c-string version of user into buf0 - copy_into_buffer(user, buf0.borrow_mut()); - // Initialze out parameters of 'libc::getpwnam_r' let mut pwd: libc::passwd = unsafe { mem::zeroed() }; let mut result: libc::passwd = unsafe { mem::zeroed() }; @@ -60,7 +64,7 @@ fn _home_dir( // Call 'libc::getpwnam_r' to write the user's home directory into buf1 let ret = unsafe { libc::getpwnam_r( - /* const char* name */ buf0.as_ptr() as *const libc::c_char, + /* const char* name */ user_ptr, /* struct passwd* pwd */ &mut pwd as *mut libc::passwd, /* char* buf */ buf1.as_mut_ptr() as *mut libc::c_char, /* size_t buflen */ buf1.len() as libc::size_t, @@ -84,7 +88,7 @@ fn _home_dir( // If `results_ptr_ptr` is null, it means libc was unable to locate the home // directory. Return a not found error. if result_ptr_ptr.is_null() { - return Err(HomeDirError::not_found(Some(user))); + return Err(HomeDirError::not_found(user)); } // Libc should ensure that the `pw.pwdir` pointer is always valid; if diff --git a/src/home_dir/other.rs b/src/home_dir/other.rs index 51b5f3a..6aa421e 100644 --- a/src/home_dir/other.rs +++ b/src/home_dir/other.rs @@ -1,16 +1,17 @@ +use std::env; use std::path::PathBuf; -use super::{HomeDirError, HomeDirErrorKind}; +use super::HomeDirError; -/// Returns the home directory of the current user if `user` is `None` or -/// an empty string. -/// -/// In the future, may also return the home directory of the provided user if -/// `user` is anything else, but that is not currently implemented for this -/// platform. +/// If `user` is `None` or an empty string, returns the value of the `$HOME`, +/// environment variable, if any. Otherwise, returns None. pub(crate) fn home_dir(user: Option<&str>) -> Result { - match user { - None | Some("") => dirs::home_dir().ok_or_else(|| HomeDirError::not_found(None)), - Some(_user) => Err(HomeDirError(HomeDirErrorKind::Unimplemented)), + if user.is_none() || user == Some("") { + if let Some(o) = env::var_os("HOME") { + if !o.is_empty() { + return Ok(PathBuf::from(o)); + } + } } + Err(HomeDirError::not_found(user)) } diff --git a/src/lib.rs b/src/lib.rs index a0743cd..c3c03bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -723,7 +723,7 @@ where mod tilde_tests { use std::path::{Path, PathBuf}; - use super::{tilde, tilde_with_context}; + use super::{home_dir, tilde, tilde_with_context}; #[test] fn test_with_tilde_no_hd() { @@ -753,7 +753,7 @@ mod tilde_tests { #[test] fn test_global_tilde() { - match dirs::home_dir() { + match home_dir::home_dir(None).ok() { Some(hd) => assert_eq!(tilde("~/something"), format!("{}/something", hd.display())), None => assert_eq!(tilde("~/something"), "~/something"), } From 30ac2557f636f87605cfd4da72fff96a2295548f Mon Sep 17 00:00:00 2001 From: Brian Myers Date: Sun, 29 Mar 2020 22:12:42 -0400 Subject: [PATCH 12/12] tiny fix --- src/home_dir/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home_dir/mod.rs b/src/home_dir/mod.rs index afa2fa3..938358f 100644 --- a/src/home_dir/mod.rs +++ b/src/home_dir/mod.rs @@ -30,7 +30,7 @@ mod other; * ******************* */ -// nix (with libc feature enabled) +// nix (with libc feature) #[cfg(all(unix, not(target_os = "redox"), feature = "libc"))] pub(crate) use self::nix::home_dir;