1use std::{borrow::Cow, path::PathBuf};
2
3use bstr::{BStr, BString, ByteSlice};
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 #[expect(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 #[expect(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 #[expect(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_bstr()
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_bstr()
106 }
107}
108
109impl From<BString> for Path {
110 fn from(mut value: BString) -> Self {
111 const OPTIONAL_PREFIX: &[u8] = b":(optional)";
113
114 if value.starts_with(OPTIONAL_PREFIX) {
115 value.drain(..OPTIONAL_PREFIX.len());
116 Path {
117 value,
118 is_optional: true,
119 }
120 } else {
121 Path {
122 value,
123 is_optional: false,
124 }
125 }
126 }
127}
128
129impl From<Cow<'_, BStr>> for Path {
130 fn from(value: Cow<'_, BStr>) -> Self {
131 Path::from(value.into_owned())
132 }
133}
134
135impl From<&BStr> for Path {
136 fn from(value: &BStr) -> Self {
137 Path::from(value.to_owned())
138 }
139}
140
141impl From<&str> for Path {
142 fn from(value: &str) -> Self {
143 Path::from(BString::from(value))
144 }
145}
146
147impl Path {
148 pub fn interpolate(
163 self,
164 interpolate::Context {
165 git_install_dir,
166 home_dir,
167 home_for_user,
168 }: interpolate::Context<'_>,
169 ) -> Result<PathBuf, interpolate::Error> {
170 if self.is_empty() {
171 return Err(interpolate::Error::Missing { what: "path" });
172 }
173
174 const PREFIX: &[u8] = b"%(prefix)/";
175 const USER_HOME: &[u8] = b"~/";
176 if self.starts_with(PREFIX) {
177 let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing {
178 what: "git install dir",
179 })?;
180 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
181 let path_without_trailing_slash =
182 gix_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
183 interpolate::Error::Utf8Conversion {
184 what: "path past %(prefix)",
185 err,
186 }
187 })?;
188 Ok(git_install_dir.join(path_without_trailing_slash))
189 } else if self.starts_with(USER_HOME) {
190 let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?;
191 let (_prefix, val) = self.split_at(USER_HOME.len());
192 let val = gix_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
193 what: "path past ~/",
194 err,
195 })?;
196 Ok(home_path.join(val))
197 } else if self.starts_with(b"~") && self.contains(&b'/') {
198 self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing {
199 what: "home for user lookup",
200 })?)
201 } else {
202 Ok(gix_path::from_bstr(self.value.as_bstr()).into_owned())
203 }
204 }
205
206 #[cfg(any(target_os = "windows", target_os = "android"))]
207 fn interpolate_user(self, _home_for_user: fn(&str) -> Option<PathBuf>) -> Result<PathBuf, interpolate::Error> {
208 Err(interpolate::Error::UserInterpolationUnsupported)
209 }
210
211 #[cfg(not(any(target_os = "windows", target_os = "android")))]
212 fn interpolate_user(self, home_for_user: fn(&str) -> Option<PathBuf>) -> Result<PathBuf, interpolate::Error> {
213 let (_prefix, val) = self.split_at("/".len());
214 let i = val
215 .iter()
216 .position(|&e| e == b'/')
217 .ok_or(interpolate::Error::Missing { what: "/" })?;
218 let (username, path_with_leading_slash) = val.split_at(i);
219 let username = std::str::from_utf8(username)?;
220 let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?;
221 let path_past_user_prefix =
222 gix_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
223 interpolate::Error::Utf8Conversion {
224 what: "path past ~user/",
225 err,
226 }
227 })?;
228 Ok(home.join(path_past_user_prefix))
229 }
230}