use std::path::{Path, PathBuf};
use bstr::{BStr, BString, ByteSlice};
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ForUser {
Current,
Name(BString),
}
impl From<ForUser> for Option<BString> {
fn from(v: ForUser) -> Self {
match v {
ForUser::Name(user) => Some(user),
ForUser::Current => None,
}
}
}
#[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<BString> },
}
fn path_segments(path: &BStr) -> Option<impl Iterator<Item = &[u8]>> {
if path.starts_with(b"/") {
Some(path[1..].split(|c| *c == b'/'))
} else {
None
}
}
pub fn parse(path: &BStr) -> Result<(Option<ForUser>, 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::<Vec<_>>().join("/")
)
.into(),
)
} else {
(None, path.into())
}
})
})
.unwrap_or_else(|| (None, path.into())))
}
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,
}
}
pub fn with(
user: Option<&ForUser>,
path: &BStr,
home_for_user: impl FnOnce(&ForUser) -> Option<PathBuf>,
) -> Result<PathBuf, Error> {
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(),
})
}