diff options
Diffstat (limited to 'vendor/gix-config-value/src/path.rs')
-rw-r--r-- | vendor/gix-config-value/src/path.rs | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/vendor/gix-config-value/src/path.rs b/vendor/gix-config-value/src/path.rs new file mode 100644 index 000000000..99ee5cf2e --- /dev/null +++ b/vendor/gix-config-value/src/path.rs @@ -0,0 +1,195 @@ +use std::{borrow::Cow, path::PathBuf}; + +use bstr::BStr; + +use crate::Path; + +/// +pub mod interpolate { + use std::path::PathBuf; + + /// Options for interpolating paths with [`Path::interpolate()`][crate::Path::interpolate()]. + #[derive(Clone, Copy)] + pub struct Context<'a> { + /// The location where gitoxide or git is installed. If `None`, `%(prefix)` in paths will cause an error. + pub git_install_dir: Option<&'a std::path::Path>, + /// The home directory of the current user. If `None`, `~/` in paths will cause an error. + pub home_dir: Option<&'a std::path::Path>, + /// A function returning the home directory of a given user. If `None`, `~name/` in paths will cause an error. + pub home_for_user: Option<fn(&str) -> Option<PathBuf>>, + } + + impl Default for Context<'_> { + fn default() -> Self { + Context { + git_install_dir: None, + home_dir: None, + home_for_user: Some(home_for_user), + } + } + } + + /// The error returned by [`Path::interpolate()`][crate::Path::interpolate()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("{} is missing", .what)] + Missing { what: &'static str }, + #[error("Ill-formed UTF-8 in {}", .what)] + Utf8Conversion { + what: &'static str, + #[source] + err: gix_path::Utf8Error, + }, + #[error("Ill-formed UTF-8 in username")] + UsernameConversion(#[from] std::str::Utf8Error), + #[error("User interpolation is not available on this platform")] + UserInterpolationUnsupported, + } + + /// Obtain the home directory for the given user `name` or return `None` if the user wasn't found + /// or any other error occurred. + /// It can be used as `home_for_user` parameter in [`Path::interpolate()`][crate::Path::interpolate()]. + #[cfg_attr(windows, allow(unused_variables))] + pub fn home_for_user(name: &str) -> Option<PathBuf> { + #[cfg(not(any(target_os = "android", target_os = "windows")))] + { + let cname = std::ffi::CString::new(name).ok()?; + // SAFETY: calling this in a threaded program that modifies the pw database is not actually safe. + // TODO: use the `*_r` version, but it's much harder to use. + #[allow(unsafe_code)] + let pwd = unsafe { libc::getpwnam(cname.as_ptr()) }; + if pwd.is_null() { + None + } else { + use std::os::unix::ffi::OsStrExt; + // SAFETY: pw_dir is a cstr and it lives as long as… well, we hope nobody changes the pw database while we are at it + // from another thread. Otherwise it lives long enough. + #[allow(unsafe_code)] + let cstr = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) }; + Some(std::ffi::OsStr::from_bytes(cstr.to_bytes()).into()) + } + } + #[cfg(any(target_os = "android", target_os = "windows"))] + { + None + } + } +} + +impl<'a> std::ops::Deref for Path<'a> { + type Target = BStr; + + fn deref(&self) -> &Self::Target { + self.value.as_ref() + } +} + +impl<'a> AsRef<[u8]> for Path<'a> { + fn as_ref(&self) -> &[u8] { + self.value.as_ref() + } +} + +impl<'a> AsRef<BStr> for Path<'a> { + fn as_ref(&self) -> &BStr { + self.value.as_ref() + } +} + +impl<'a> From<Cow<'a, BStr>> for Path<'a> { + fn from(value: Cow<'a, BStr>) -> Self { + Path { value } + } +} + +impl<'a> Path<'a> { + /// Interpolates this path into a path usable on the file system. + /// + /// If this path starts with `~/` or `~user/` or `%(prefix)/` + /// - `~/` is expanded to the value of `home_dir`. The caller can use the [dirs](https://crates.io/crates/dirs) crate to obtain it. + /// It it is required but not set, an error is produced. + /// - `~user/` to the specified user’s home directory, e.g `~alice` might get expanded to `/home/alice` on linux, but requires + /// the `home_for_user` function to be provided. + /// The interpolation uses `getpwnam` sys call and is therefore not available on windows. + /// - `%(prefix)/` is expanded to the location where `gitoxide` is installed. + /// This location is not known at compile time and therefore need to be + /// optionally provided by the caller through `git_install_dir`. + /// + /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value or if required input + /// wasn't provided. + pub fn interpolate( + self, + interpolate::Context { + git_install_dir, + home_dir, + home_for_user, + }: interpolate::Context<'_>, + ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { + if self.is_empty() { + return Err(interpolate::Error::Missing { what: "path" }); + } + + const PREFIX: &[u8] = b"%(prefix)/"; + const USER_HOME: &[u8] = b"~/"; + if self.starts_with(PREFIX) { + let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing { + what: "git install dir", + })?; + let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); + let path_without_trailing_slash = + gix_path::try_from_bstring(path_without_trailing_slash).map_err(|err| { + interpolate::Error::Utf8Conversion { + what: "path past %(prefix)", + err, + } + })?; + Ok(git_install_dir.join(path_without_trailing_slash).into()) + } else if self.starts_with(USER_HOME) { + let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?; + let (_prefix, val) = self.split_at(USER_HOME.len()); + let val = gix_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion { + what: "path past ~/", + err, + })?; + Ok(home_path.join(val).into()) + } else if self.starts_with(b"~") && self.contains(&b'/') { + self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing { + what: "home for user lookup", + })?) + } else { + Ok(gix_path::from_bstr(self.value)) + } + } + + #[cfg(any(target_os = "windows", target_os = "android"))] + fn interpolate_user( + self, + _home_for_user: fn(&str) -> Option<PathBuf>, + ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { + Err(interpolate::Error::UserInterpolationUnsupported) + } + + #[cfg(not(any(target_os = "windows", target_os = "android")))] + fn interpolate_user( + self, + home_for_user: fn(&str) -> Option<PathBuf>, + ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { + let (_prefix, val) = self.split_at("/".len()); + let i = val + .iter() + .position(|&e| e == b'/') + .ok_or(interpolate::Error::Missing { what: "/" })?; + let (username, path_with_leading_slash) = val.split_at(i); + let username = std::str::from_utf8(username)?; + let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?; + let path_past_user_prefix = + gix_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| { + interpolate::Error::Utf8Conversion { + what: "path past ~user/", + err, + } + })?; + Ok(home.join(path_past_user_prefix).into()) + } +} |