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: gix_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 #[cfg_attr(all(target_family = "wasm", not(target_os = "emscripten")), allow(unused_variables))]
55 pub fn home_for_user(name: &str) -> Option<PathBuf> {
56 #[cfg(not(any(
57 target_os = "android",
58 target_os = "windows",
59 all(target_family = "wasm", not(target_os = "emscripten"))
60 )))]
61 {
62 let cname = std::ffi::CString::new(name).ok()?;
63 #[allow(unsafe_code)]
66 let pwd = unsafe { libc::getpwnam(cname.as_ptr()) };
67 if pwd.is_null() {
68 None
69 } else {
70 use std::os::unix::ffi::OsStrExt;
71 #[allow(unsafe_code)]
74 let cstr = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) };
75 Some(std::ffi::OsStr::from_bytes(cstr.to_bytes()).into())
76 }
77 }
78 #[cfg(any(
79 target_os = "android",
80 target_os = "windows",
81 all(target_family = "wasm", not(target_os = "emscripten"))
82 ))]
83 {
84 None
85 }
86 }
87}
88
89impl std::ops::Deref for Path<'_> {
90 type Target = BStr;
91
92 fn deref(&self) -> &Self::Target {
93 self.value.as_ref()
94 }
95}
96
97impl AsRef<[u8]> for Path<'_> {
98 fn as_ref(&self) -> &[u8] {
99 self.value.as_ref()
100 }
101}
102
103impl AsRef<BStr> for Path<'_> {
104 fn as_ref(&self) -> &BStr {
105 self.value.as_ref()
106 }
107}
108
109impl<'a> From<Cow<'a, BStr>> for Path<'a> {
110 fn from(value: Cow<'a, BStr>) -> Self {
111 const OPTIONAL_PREFIX: &[u8] = b":(optional)";
113
114 if value.starts_with(OPTIONAL_PREFIX) {
115 let stripped = match value {
119 Cow::Borrowed(b) => Cow::Borrowed(&b[OPTIONAL_PREFIX.len()..]),
120 Cow::Owned(mut b) => {
121 b.drain(..OPTIONAL_PREFIX.len());
122 Cow::Owned(b)
123 }
124 };
125 Path {
126 value: stripped,
127 is_optional: true,
128 }
129 } else {
130 Path {
131 value,
132 is_optional: false,
133 }
134 }
135 }
136}
137
138impl<'a> Path<'a> {
139 pub fn interpolate(
154 self,
155 interpolate::Context {
156 git_install_dir,
157 home_dir,
158 home_for_user,
159 }: interpolate::Context<'_>,
160 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
161 if self.is_empty() {
162 return Err(interpolate::Error::Missing { what: "path" });
163 }
164
165 const PREFIX: &[u8] = b"%(prefix)/";
166 const USER_HOME: &[u8] = b"~/";
167 if self.starts_with(PREFIX) {
168 let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing {
169 what: "git install dir",
170 })?;
171 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
172 let path_without_trailing_slash =
173 gix_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
174 interpolate::Error::Utf8Conversion {
175 what: "path past %(prefix)",
176 err,
177 }
178 })?;
179 Ok(git_install_dir.join(path_without_trailing_slash).into())
180 } else if self.starts_with(USER_HOME) {
181 let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?;
182 let (_prefix, val) = self.split_at(USER_HOME.len());
183 let val = gix_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
184 what: "path past ~/",
185 err,
186 })?;
187 Ok(home_path.join(val).into())
188 } else if self.starts_with(b"~") && self.contains(&b'/') {
189 self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing {
190 what: "home for user lookup",
191 })?)
192 } else {
193 Ok(gix_path::from_bstr(self.value))
194 }
195 }
196
197 #[cfg(any(target_os = "windows", target_os = "android"))]
198 fn interpolate_user(
199 self,
200 _home_for_user: fn(&str) -> Option<PathBuf>,
201 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
202 Err(interpolate::Error::UserInterpolationUnsupported)
203 }
204
205 #[cfg(not(any(target_os = "windows", target_os = "android")))]
206 fn interpolate_user(
207 self,
208 home_for_user: fn(&str) -> Option<PathBuf>,
209 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
210 let (_prefix, val) = self.split_at("/".len());
211 let i = val
212 .iter()
213 .position(|&e| e == b'/')
214 .ok_or(interpolate::Error::Missing { what: "/" })?;
215 let (username, path_with_leading_slash) = val.split_at(i);
216 let username = std::str::from_utf8(username)?;
217 let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?;
218 let path_past_user_prefix =
219 gix_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
220 interpolate::Error::Utf8Conversion {
221 what: "path past ~user/",
222 err,
223 }
224 })?;
225 Ok(home.join(path_past_user_prefix).into())
226 }
227}