1use std::path::{Path, PathBuf};
3
4use bstr::{BStr, BString, ByteSlice};
5
6#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum ForUser {
10 Current,
12 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#[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
43pub 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
74pub 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
94pub 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}