hanzo_utils_absolute_path/
lib.rs1use dirs::home_dir;
2use path_absolutize::Absolutize;
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde::Deserializer;
6use serde::Serialize;
7use serde::de::Error as SerdeError;
8use std::cell::RefCell;
9use std::path::Display;
10use std::path::Path;
11use std::path::PathBuf;
12use ts_rs::TS;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema, TS)]
22pub struct AbsolutePathBuf(PathBuf);
23
24impl AbsolutePathBuf {
25 fn maybe_expand_home_directory(path: &Path) -> PathBuf {
26 let Some(path_str) = path.to_str() else {
27 return path.to_path_buf();
28 };
29 if cfg!(not(target_os = "windows"))
30 && let Some(home) = home_dir()
31 {
32 if path_str == "~" {
33 return home;
34 }
35 if let Some(rest) = path_str.strip_prefix("~/") {
36 let rest = rest.trim_start_matches('/');
37 if rest.is_empty() {
38 return home;
39 }
40 return home.join(rest);
41 }
42 }
43 path.to_path_buf()
44 }
45
46 pub fn resolve_path_against_base<P: AsRef<Path>, B: AsRef<Path>>(
47 path: P,
48 base_path: B,
49 ) -> std::io::Result<Self> {
50 let expanded = Self::maybe_expand_home_directory(path.as_ref());
51 let absolute_path = expanded.absolutize_from(base_path.as_ref())?;
52 Ok(Self(absolute_path.into_owned()))
53 }
54
55 pub fn from_absolute_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
56 let expanded = Self::maybe_expand_home_directory(path.as_ref());
57 let absolute_path = expanded.absolutize()?;
58 Ok(Self(absolute_path.into_owned()))
59 }
60
61 pub fn current_dir() -> std::io::Result<Self> {
62 let current_dir = std::env::current_dir()?;
63 Self::from_absolute_path(current_dir)
64 }
65
66 pub fn join<P: AsRef<Path>>(&self, path: P) -> std::io::Result<Self> {
67 Self::resolve_path_against_base(path, &self.0)
68 }
69
70 pub fn parent(&self) -> Option<Self> {
71 self.0.parent().map(|p| {
72 #[expect(clippy::expect_used)]
73 Self::from_absolute_path(p).expect("parent of AbsolutePathBuf must be absolute")
74 })
75 }
76
77 pub fn as_path(&self) -> &Path {
78 &self.0
79 }
80
81 pub fn into_path_buf(self) -> PathBuf {
82 self.0
83 }
84
85 pub fn to_path_buf(&self) -> PathBuf {
86 self.0.clone()
87 }
88
89 pub fn to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
90 self.0.to_string_lossy()
91 }
92
93 pub fn display(&self) -> Display<'_> {
94 self.0.display()
95 }
96}
97
98impl AsRef<Path> for AbsolutePathBuf {
99 fn as_ref(&self) -> &Path {
100 &self.0
101 }
102}
103
104impl From<AbsolutePathBuf> for PathBuf {
105 fn from(path: AbsolutePathBuf) -> Self {
106 path.into_path_buf()
107 }
108}
109
110impl TryFrom<&Path> for AbsolutePathBuf {
111 type Error = std::io::Error;
112
113 fn try_from(value: &Path) -> Result<Self, Self::Error> {
114 Self::from_absolute_path(value)
115 }
116}
117
118impl TryFrom<PathBuf> for AbsolutePathBuf {
119 type Error = std::io::Error;
120
121 fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
122 Self::from_absolute_path(value)
123 }
124}
125
126impl TryFrom<&str> for AbsolutePathBuf {
127 type Error = std::io::Error;
128
129 fn try_from(value: &str) -> Result<Self, Self::Error> {
130 Self::from_absolute_path(value)
131 }
132}
133
134impl TryFrom<String> for AbsolutePathBuf {
135 type Error = std::io::Error;
136
137 fn try_from(value: String) -> Result<Self, Self::Error> {
138 Self::from_absolute_path(value)
139 }
140}
141
142thread_local! {
143 static ABSOLUTE_PATH_BASE: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
144}
145
146pub struct AbsolutePathBufGuard;
151
152impl AbsolutePathBufGuard {
153 pub fn new(base_path: &Path) -> Self {
154 ABSOLUTE_PATH_BASE.with(|cell| {
155 *cell.borrow_mut() = Some(base_path.to_path_buf());
156 });
157 Self
158 }
159}
160
161impl Drop for AbsolutePathBufGuard {
162 fn drop(&mut self) {
163 ABSOLUTE_PATH_BASE.with(|cell| {
164 *cell.borrow_mut() = None;
165 });
166 }
167}
168
169impl<'de> Deserialize<'de> for AbsolutePathBuf {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: Deserializer<'de>,
173 {
174 let path = PathBuf::deserialize(deserializer)?;
175 ABSOLUTE_PATH_BASE.with(|cell| match cell.borrow().as_deref() {
176 Some(base) => {
177 Ok(Self::resolve_path_against_base(path, base).map_err(SerdeError::custom)?)
178 }
179 None if path.is_absolute() => {
180 Self::from_absolute_path(path).map_err(SerdeError::custom)
181 }
182 None => Err(SerdeError::custom(
183 "AbsolutePathBuf deserialized without a base path",
184 )),
185 })
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use pretty_assertions::assert_eq;
193 use tempfile::tempdir;
194
195 #[test]
196 fn create_with_absolute_path_ignores_base_path() {
197 let base_dir = tempdir().expect("base dir");
198 let absolute_dir = tempdir().expect("absolute dir");
199 let base_path = base_dir.path();
200 let absolute_path = absolute_dir.path().join("file.txt");
201 let abs_path_buf =
202 AbsolutePathBuf::resolve_path_against_base(absolute_path.clone(), base_path)
203 .expect("failed to create");
204 assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
205 }
206
207 #[test]
208 fn relative_path_is_resolved_against_base_path() {
209 let temp_dir = tempdir().expect("base dir");
210 let base_dir = temp_dir.path();
211 let abs_path_buf = AbsolutePathBuf::resolve_path_against_base("file.txt", base_dir)
212 .expect("failed to create");
213 assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
214 }
215
216 #[test]
217 fn guard_used_in_deserialization() {
218 let temp_dir = tempdir().expect("base dir");
219 let base_dir = temp_dir.path();
220 let relative_path = "subdir/file.txt";
221 let abs_path_buf = {
222 let _guard = AbsolutePathBufGuard::new(base_dir);
223 serde_json::from_str::<AbsolutePathBuf>(&format!(r#""{relative_path}""#))
224 .expect("failed to deserialize")
225 };
226 assert_eq!(
227 abs_path_buf.as_path(),
228 base_dir.join(relative_path).as_path()
229 );
230 }
231
232 #[cfg(not(target_os = "windows"))]
233 #[test]
234 fn home_directory_root_on_non_windows_is_expanded_in_deserialization() {
235 let Some(home) = home_dir() else {
236 return;
237 };
238 let temp_dir = tempdir().expect("base dir");
239 let abs_path_buf = {
240 let _guard = AbsolutePathBufGuard::new(temp_dir.path());
241 serde_json::from_str::<AbsolutePathBuf>("\"~\"").expect("failed to deserialize")
242 };
243 assert_eq!(abs_path_buf.as_path(), home.as_path());
244 }
245
246 #[cfg(not(target_os = "windows"))]
247 #[test]
248 fn home_directory_subpath_on_non_windows_is_expanded_in_deserialization() {
249 let Some(home) = home_dir() else {
250 return;
251 };
252 let temp_dir = tempdir().expect("base dir");
253 let abs_path_buf = {
254 let _guard = AbsolutePathBufGuard::new(temp_dir.path());
255 serde_json::from_str::<AbsolutePathBuf>("\"~/code\"").expect("failed to deserialize")
256 };
257 assert_eq!(abs_path_buf.as_path(), home.join("code").as_path());
258 }
259
260 #[cfg(not(target_os = "windows"))]
261 #[test]
262 fn home_directory_double_slash_on_non_windows_is_expanded_in_deserialization() {
263 let Some(home) = home_dir() else {
264 return;
265 };
266 let temp_dir = tempdir().expect("base dir");
267 let abs_path_buf = {
268 let _guard = AbsolutePathBufGuard::new(temp_dir.path());
269 serde_json::from_str::<AbsolutePathBuf>("\"~//code\"").expect("failed to deserialize")
270 };
271 assert_eq!(abs_path_buf.as_path(), home.join("code").as_path());
272 }
273
274 #[cfg(target_os = "windows")]
275 #[test]
276 fn home_directory_on_windows_is_not_expanded_in_deserialization() {
277 let temp_dir = tempdir().expect("base dir");
278 let base_dir = temp_dir.path();
279 let abs_path_buf = {
280 let _guard = AbsolutePathBufGuard::new(base_dir);
281 serde_json::from_str::<AbsolutePathBuf>("\"~/code\"").expect("failed to deserialize")
282 };
283 assert_eq!(
284 abs_path_buf.as_path(),
285 base_dir.join("~").join("code").as_path()
286 );
287 }
288}
289