1use std::{borrow::Cow, path::PathBuf};
2
3use bstr::{BStr, BString, ByteSlice};
4use gix_error::{ErrorExt, ExnResult, OptionExt, ResultExt, not_found, validation};
5
6use crate::Path;
7
8pub mod interpolate {
10 use std::path::PathBuf;
11
12 #[derive(Clone, Copy)]
14 pub struct Context<'a> {
15 pub git_install_dir: Option<&'a std::path::Path>,
17 pub home_dir: Option<&'a std::path::Path>,
19 pub home_for_user: Option<fn(&str) -> Option<PathBuf>>,
22 }
23
24 impl Default for Context<'_> {
25 fn default() -> Self {
26 Context {
27 git_install_dir: None,
28 home_dir: None,
29 home_for_user: Some(home_for_user),
30 }
31 }
32 }
33
34 #[cfg_attr(windows, allow(unused_variables))]
39 #[cfg_attr(all(target_family = "wasm", not(target_os = "emscripten")), allow(unused_variables))]
40 pub fn home_for_user(name: &str) -> Option<PathBuf> {
41 #[cfg(not(any(
42 target_os = "android",
43 target_os = "windows",
44 all(target_family = "wasm", not(target_os = "emscripten"))
45 )))]
46 {
47 let cname = std::ffi::CString::new(name).ok()?;
48 #[expect(unsafe_code)]
51 let pwd = unsafe { libc::getpwnam(cname.as_ptr()) };
52 if pwd.is_null() {
53 None
54 } else {
55 use std::os::unix::ffi::OsStrExt;
56 #[expect(unsafe_code)]
59 let cstr = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) };
60 Some(std::ffi::OsStr::from_bytes(cstr.to_bytes()).into())
61 }
62 }
63 #[cfg(any(
64 target_os = "android",
65 target_os = "windows",
66 all(target_family = "wasm", not(target_os = "emscripten"))
67 ))]
68 {
69 None
70 }
71 }
72}
73
74impl std::ops::Deref for Path {
75 type Target = BStr;
76
77 fn deref(&self) -> &Self::Target {
78 self.value.as_bstr()
79 }
80}
81
82impl AsRef<[u8]> for Path {
83 fn as_ref(&self) -> &[u8] {
84 self.value.as_ref()
85 }
86}
87
88impl AsRef<BStr> for Path {
89 fn as_ref(&self) -> &BStr {
90 self.value.as_bstr()
91 }
92}
93
94impl From<BString> for Path {
95 fn from(mut value: BString) -> Self {
96 const OPTIONAL_PREFIX: &[u8] = b":(optional)";
98
99 if value.starts_with(OPTIONAL_PREFIX) {
100 value.drain(..OPTIONAL_PREFIX.len());
101 Path {
102 value,
103 is_optional: true,
104 }
105 } else {
106 Path {
107 value,
108 is_optional: false,
109 }
110 }
111 }
112}
113
114impl From<Cow<'_, BStr>> for Path {
115 fn from(value: Cow<'_, BStr>) -> Self {
116 Path::from(value.into_owned())
117 }
118}
119
120impl From<&BStr> for Path {
121 fn from(value: &BStr) -> Self {
122 Path::from(value.to_owned())
123 }
124}
125
126impl From<&str> for Path {
127 fn from(value: &str) -> Self {
128 Path::from(BString::from(value))
129 }
130}
131
132impl Path {
133 pub fn interpolate(
150 self,
151 interpolate::Context {
152 git_install_dir,
153 home_dir,
154 home_for_user,
155 }: interpolate::Context<'_>,
156 ) -> ExnResult<PathBuf> {
157 if self.is_empty() {
158 return Err(not_found("path is missing").raise_erased());
159 }
160
161 const PREFIX: &[u8] = b"%(prefix)/";
162 if self.starts_with(PREFIX) {
163 let git_install_dir = git_install_dir.ok_or_raise_erased(|| not_found("git install dir is missing"))?;
164 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
165 let path_without_trailing_slash =
166 gix_path::try_from_bstring(path_without_trailing_slash).or_raise_erased(|| {
167 validation("Ill-formed UTF-8 in path past %(prefix)").with("input", path_without_trailing_slash)
168 })?;
169 Ok(git_install_dir.join(path_without_trailing_slash))
170 } else if let Some(val) = self.strip_prefix(b"~") {
171 let (username, path) = match val.split_once_str(b"/") {
172 Some((username, path)) => (username, Some(path)),
173 None => (val, None),
174 };
175 let (mut home, what) = if username.is_empty() {
176 (
177 home_dir
178 .ok_or_raise_erased(|| not_found("home dir is missing"))?
179 .to_path_buf(),
180 "path past ~/",
181 )
182 } else {
183 (
184 Self::home_for_username(
185 username,
186 home_for_user.ok_or_raise_erased(|| not_found("home for user lookup is missing"))?,
187 )?,
188 "path past ~user/",
189 )
190 };
191 if let Some(path) = path {
192 home.push(
193 gix_path::try_from_byte_slice(path)
194 .or_raise_erased(|| validation(format!("Ill-formed UTF-8 in {what}")).with("input", path))?,
195 );
196 }
197 Ok(home)
198 } else {
199 Ok(gix_path::from_bstr(self.value.as_bstr()).into_owned())
200 }
201 }
202
203 fn home_for_username(username: &[u8], home_for_user: fn(&str) -> Option<PathBuf>) -> ExnResult<PathBuf> {
204 let username = std::str::from_utf8(username)
205 .or_raise_erased(|| validation("Ill-formed UTF-8 in username").with("input", username))?;
206 home_for_user(username).ok_or_raise_erased(|| not_found("pwd user info is missing"))
207 }
208}