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 Path { value }
112 }
113}
114
115impl<'a> Path<'a> {
116 pub fn interpolate(
131 self,
132 interpolate::Context {
133 git_install_dir,
134 home_dir,
135 home_for_user,
136 }: interpolate::Context<'_>,
137 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
138 if self.is_empty() {
139 return Err(interpolate::Error::Missing { what: "path" });
140 }
141
142 const PREFIX: &[u8] = b"%(prefix)/";
143 const USER_HOME: &[u8] = b"~/";
144 if self.starts_with(PREFIX) {
145 let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing {
146 what: "git install dir",
147 })?;
148 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
149 let path_without_trailing_slash =
150 gix_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
151 interpolate::Error::Utf8Conversion {
152 what: "path past %(prefix)",
153 err,
154 }
155 })?;
156 Ok(git_install_dir.join(path_without_trailing_slash).into())
157 } else if self.starts_with(USER_HOME) {
158 let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?;
159 let (_prefix, val) = self.split_at(USER_HOME.len());
160 let val = gix_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
161 what: "path past ~/",
162 err,
163 })?;
164 Ok(home_path.join(val).into())
165 } else if self.starts_with(b"~") && self.contains(&b'/') {
166 self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing {
167 what: "home for user lookup",
168 })?)
169 } else {
170 Ok(gix_path::from_bstr(self.value))
171 }
172 }
173
174 #[cfg(any(target_os = "windows", target_os = "android"))]
175 fn interpolate_user(
176 self,
177 _home_for_user: fn(&str) -> Option<PathBuf>,
178 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
179 Err(interpolate::Error::UserInterpolationUnsupported)
180 }
181
182 #[cfg(not(any(target_os = "windows", target_os = "android")))]
183 fn interpolate_user(
184 self,
185 home_for_user: fn(&str) -> Option<PathBuf>,
186 ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
187 let (_prefix, val) = self.split_at("/".len());
188 let i = val
189 .iter()
190 .position(|&e| e == b'/')
191 .ok_or(interpolate::Error::Missing { what: "/" })?;
192 let (username, path_with_leading_slash) = val.split_at(i);
193 let username = std::str::from_utf8(username)?;
194 let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?;
195 let path_past_user_prefix =
196 gix_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
197 interpolate::Error::Utf8Conversion {
198 what: "path past ~user/",
199 err,
200 }
201 })?;
202 Ok(home.join(path_past_user_prefix).into())
203 }
204}