| use std::{borrow::Cow, path::PathBuf}; |
| |
| use bstr::BStr; |
| |
| use crate::Path; |
| |
| /// |
| #[allow(clippy::empty_docs)] |
| 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. |
| /// If 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()) |
| } |
| } |