gix_url/
expand_path.rs

1//! Functions for expanding repository paths.
2use std::path::{Path, PathBuf};
3
4use bstr::{BStr, BString, ByteSlice};
5
6/// Whether a repository is resolving for the current user, or the given one.
7#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum ForUser {
10    /// The currently logged in user.
11    Current,
12    /// The user with the given name.
13    Name(BString),
14}
15
16impl From<ForUser> for Option<BString> {
17    fn from(v: ForUser) -> Self {
18        match v {
19            ForUser::Name(user) => Some(user),
20            ForUser::Current => None,
21        }
22    }
23}
24
25/// The error used by [`parse()`], [`with()`] and [`expand_path()`](crate::expand_path()).
26#[derive(Debug, thiserror::Error)]
27#[allow(missing_docs)]
28pub enum Error {
29    #[error("UTF8 conversion on non-unix system failed for path: {path:?}")]
30    IllformedUtf8 { path: BString },
31    #[error("Home directory could not be obtained for {}", match user {Some(user) => format!("user '{user}'"), None => "current user".into()})]
32    MissingHome { user: Option<BString> },
33}
34
35fn path_segments(path: &BStr) -> Option<impl Iterator<Item = &[u8]>> {
36    if path.starts_with(b"/") {
37        Some(path[1..].split(|c| *c == b'/'))
38    } else {
39        None
40    }
41}
42
43/// Parse user information from the given `path`, returning `(possible user information, adjusted input path)`.
44///
45/// Supported formats for user extraction areā€¦
46/// * `~/repopath` - the currently logged in user's home.
47/// * `~user/repopath` - the repository in the given user's home.
48pub fn parse(path: &BStr) -> Result<(Option<ForUser>, BString), Error> {
49    Ok(path_segments(path)
50        .and_then(|mut iter| {
51            iter.next().map(|segment| {
52                if segment.starts_with(b"~") {
53                    let eu = if segment.len() == 1 {
54                        Some(ForUser::Current)
55                    } else {
56                        Some(ForUser::Name(segment[1..].into()))
57                    };
58                    (
59                        eu,
60                        format!(
61                            "/{}",
62                            iter.map(|s| s.as_bstr().to_str_lossy()).collect::<Vec<_>>().join("/")
63                        )
64                        .into(),
65                    )
66                } else {
67                    (None, path.into())
68                }
69            })
70        })
71        .unwrap_or_else(|| (None, path.into())))
72}
73
74/// Expand `path` for use in a shell and return the expanded path.
75pub fn for_shell(path: BString) -> BString {
76    use bstr::ByteVec;
77    match parse(path.as_slice().as_bstr()) {
78        Ok((user, mut path)) => match user {
79            Some(ForUser::Current) => {
80                path.insert(0, b'~');
81                path
82            }
83            Some(ForUser::Name(mut user)) => {
84                user.insert(0, b'~');
85                user.append(path.as_vec_mut());
86                user
87            }
88            None => path,
89        },
90        Err(_) => path,
91    }
92}
93
94/// Expand `path` for the given `user`, which can be obtained by [`parse()`], resolving them with `home_for_user(&user)`.
95///
96/// For the common case consider using [`expand_path()]` instead.
97pub fn with(
98    user: Option<&ForUser>,
99    path: &BStr,
100    home_for_user: impl FnOnce(&ForUser) -> Option<PathBuf>,
101) -> Result<PathBuf, Error> {
102    fn make_relative(path: &Path) -> PathBuf {
103        path.components().skip(1).collect()
104    }
105    let path = gix_path::try_from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?;
106    Ok(match user {
107        Some(user) => home_for_user(user)
108            .ok_or_else(|| Error::MissingHome {
109                user: user.to_owned().into(),
110            })?
111            .join(make_relative(path)),
112        None => path.into(),
113    })
114}