1use std::{borrow::Cow, path::PathBuf};
2
3use bstr::BStr;
4
5use crate::Path;
6
7pub mod interpolate {
9 use std::path::PathBuf;
10
11 #[derive(Clone, Copy)]
13 pub struct Context<'a> {
14 pub git_install_dir: Option<&'a std::path::Path>,
16 pub home_dir: Option<&'a std::path::Path>,
18 pub home_for_user: Option<fn(&str) -> Option<PathBuf>>,
20 }
21
22 impl Default for Context<'_> {
23 fn default() -> Self {
24 Context {
25 git_install_dir: None,
26 home_dir: None,
27 home_for_user: Some(home_for_user),
28 }
29 }
30 }
31
32 #[derive(Debug, thiserror::Error)]
34 #[allow(missing_docs)]
35 pub enum Error {
36 #[error("{} is missing", .what)]
37 Missing { what: &'static str },
38 #[error("Ill-formed UTF-8 in {}", .what)]
39 Utf8Conversion {
40 what: &'static str,
41 #[source]
42 err: git_path::Utf8Error,
43 },
44 #[error("Ill-formed UTF-8 in username")]
45 UsernameConversion(#[from] std::str::Utf8Error),
46 #[error("User interpolation is not available on this platform")]
47 UserInterpolationUnsupported,
48 }
49
50 #[cfg_attr(windows, allow(unused_variables))]
54 pub fn home_for_user(name: &str) -> Option<PathBuf> {
55 #[cfg(not(any(target_os = "android", target_os = "windows")))]
56 {
57 let cname = std::ffi::CString::new(name).ok()?;
58 #[allow(unsafe_code)]
61 let pwd = unsafe { libc::getpwnam(cname.as_ptr()) };
62 if pwd.is_null() {
63 None
64 } else {
65 use std::os::unix::ffi::OsStrExt;
66 #[allow(unsafe_code)]
69 let cstr = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) };
70 Some(std::ffi::OsStr::from_bytes(cstr.to_bytes()).into())
71 }
72 }
73 #[cfg(any(target_os = "android", target_os = "windows"))]
74 {
75 None
76 }
77 }
78}
79
80impl<'a> std::ops::Deref for Path<'a> {
81 type Target = BStr;
82
83 fn deref(&self) -> &Self::Target {
84 self.value.as_ref()
85 }
86}
87
88impl<'a> AsRef<[u8]> for Path<'a> {
89 fn as_ref(&self) -> &[u8] {
90 self.value.as_ref()
91 }
92}
93
94impl<'a> AsRef<BStr> for Path<'a> {
95 fn as_ref(&self) -> &BStr {
96 self.value.as_ref()
97 }
98}
99
100impl<'a> From<Cow<'a, BStr>> for Path<'a> {
101 fn from(value: Cow<'a, BStr>) -> Self {
102 Path { value }
103 }
104}
105
106impl<'a> Path<'a> {
107 pub fn interpolate(
122 self,
123 interpolate::Context {
124 git_install_dir,
125 home_dir,
126 home_for_user,
127 }: interpolate::Context<'_>,
128 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
129 if self.is_empty() {
130 return Err(interpolate::Error::Missing { what: "path" });
131 }
132
133 const PREFIX: &[u8] = b"%(prefix)/";
134 const USER_HOME: &[u8] = b"~/";
135 if self.starts_with(PREFIX) {
136 let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing {
137 what: "git install dir",
138 })?;
139 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
140 let path_without_trailing_slash =
141 git_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
142 interpolate::Error::Utf8Conversion {
143 what: "path past %(prefix)",
144 err,
145 }
146 })?;
147 Ok(git_install_dir.join(path_without_trailing_slash).into())
148 } else if self.starts_with(USER_HOME) {
149 let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?;
150 let (_prefix, val) = self.split_at(USER_HOME.len());
151 let val = git_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
152 what: "path past ~/",
153 err,
154 })?;
155 Ok(home_path.join(val).into())
156 } else if self.starts_with(b"~") && self.contains(&b'/') {
157 self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing {
158 what: "home for user lookup",
159 })?)
160 } else {
161 Ok(git_path::from_bstr(self.value))
162 }
163 }
164
165 #[cfg(any(target_os = "windows", target_os = "android"))]
166 fn interpolate_user(
167 self,
168 _home_for_user: fn(&str) -> Option<PathBuf>,
169 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
170 Err(interpolate::Error::UserInterpolationUnsupported)
171 }
172
173 #[cfg(not(any(target_os = "windows", target_os = "android")))]
174 fn interpolate_user(
175 self,
176 home_for_user: fn(&str) -> Option<PathBuf>,
177 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
178 let (_prefix, val) = self.split_at("/".len());
179 let i = val
180 .iter()
181 .position(|&e| e == b'/')
182 .ok_or(interpolate::Error::Missing { what: "/" })?;
183 let (username, path_with_leading_slash) = val.split_at(i);
184 let username = std::str::from_utf8(username)?;
185 let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?;
186 let path_past_user_prefix =
187 git_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
188 interpolate::Error::Utf8Conversion {
189 what: "path past ~user/",
190 err,
191 }
192 })?;
193 Ok(home.join(path_past_user_prefix).into())
194 }
195}