//! Functions for expanding repository paths. use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice}; /// Whether a repository is resolving for the current user, or the given one. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ForUser { /// The currently logged in user. Current, /// The user with the given name. Name(BString), } impl From for Option { fn from(v: ForUser) -> Self { match v { ForUser::Name(user) => Some(user), ForUser::Current => None, } } } /// The error used by [`parse()`], [`with()`] and [`expand_path()`](crate::expand_path()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("UTF8 conversion on non-unix system failed for path: {path:?}")] IllformedUtf8 { path: BString }, #[error("Home directory could not be obtained for {}", match user {Some(user) => format!("user '{user}'"), None => "current user".into()})] MissingHome { user: Option }, } fn path_segments(path: &BStr) -> Option> { if path.starts_with(b"/") { Some(path[1..].split(|c| *c == b'/')) } else { None } } /// Parse user information from the given `path`, returning `(possible user information, adjusted input path)`. /// /// Supported formats for user extraction areā€¦ /// * `~/repopath` - the currently logged in user's home. /// * `~user/repopath` - the repository in the given user's home. pub fn parse(path: &BStr) -> Result<(Option, BString), Error> { Ok(path_segments(path) .and_then(|mut iter| { iter.next().map(|segment| { if segment.starts_with(b"~") { let eu = if segment.len() == 1 { Some(ForUser::Current) } else { Some(ForUser::Name(segment[1..].into())) }; ( eu, format!( "/{}", iter.map(|s| s.as_bstr().to_str_lossy()).collect::>().join("/") ) .into(), ) } else { (None, path.into()) } }) }) .unwrap_or_else(|| (None, path.into()))) } /// Expand `path` for use in a shell and return the expanded path. pub fn for_shell(path: BString) -> BString { use bstr::ByteVec; match parse(path.as_slice().as_bstr()) { Ok((user, mut path)) => match user { Some(ForUser::Current) => { path.insert(0, b'~'); path } Some(ForUser::Name(mut user)) => { user.insert(0, b'~'); user.append(path.as_vec_mut()); user } None => path, }, Err(_) => path, } } /// Expand `path` for the given `user`, which can be obtained by [`parse()`], resolving them with `home_for_user(&user)`. /// /// For the common case consider using [`expand_path()]` instead. pub fn with( user: Option<&ForUser>, path: &BStr, home_for_user: impl FnOnce(&ForUser) -> Option, ) -> Result { fn make_relative(path: &Path) -> PathBuf { path.components().skip(1).collect() } let path = gix_path::try_from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?; Ok(match user { Some(user) => home_for_user(user) .ok_or_else(|| Error::MissingHome { user: user.to_owned().into(), })? .join(make_relative(path)), None => path.into(), }) }