use std::{borrow::Cow, path::PathBuf};
use bstr::BStr;
use crate::Path;
pub mod interpolate {
use std::path::PathBuf;
#[derive(Clone, Copy)]
pub struct Context<'a> {
pub git_install_dir: Option<&'a std::path::Path>,
pub home_dir: Option<&'a std::path::Path>,
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),
}
}
}
#[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,
}
#[cfg_attr(windows, allow(unused_variables))]
#[cfg_attr(all(target_family = "wasm", not(target_os = "emscripten")), allow(unused_variables))]
pub fn home_for_user(name: &str) -> Option<PathBuf> {
#[cfg(not(any(
target_os = "android",
target_os = "windows",
all(target_family = "wasm", not(target_os = "emscripten"))
)))]
{
let cname = std::ffi::CString::new(name).ok()?;
#[allow(unsafe_code)]
let pwd = unsafe { libc::getpwnam(cname.as_ptr()) };
if pwd.is_null() {
None
} else {
use std::os::unix::ffi::OsStrExt;
#[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",
all(target_family = "wasm", not(target_os = "emscripten"))
))]
{
None
}
}
}
impl std::ops::Deref for Path<'_> {
type Target = BStr;
fn deref(&self) -> &Self::Target {
self.value.as_ref()
}
}
impl AsRef<[u8]> for Path<'_> {
fn as_ref(&self) -> &[u8] {
self.value.as_ref()
}
}
impl AsRef<BStr> for Path<'_> {
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> {
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())
}
}