1use anyhow::{anyhow, Context, Result};
6use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
7use pathdiff::diff_utf8_paths;
8
9pub fn path_relative_from(
17 path: impl AsRef<Utf8Path>,
18 base: impl AsRef<Utf8Path>,
19) -> Result<Utf8PathBuf> {
20 fn inner(path: &Utf8Path, base: &Utf8Path) -> Result<Utf8PathBuf> {
21 let path = normalized_absolute_path(path)
22 .with_context(|| format!("converting path to normalized absolute path: {path}"))?;
23
24 let base = normalized_absolute_path(base)
25 .with_context(|| format!("converting base to normalized absolute path: {base}"))?;
26
27 diff_utf8_paths(&path, &base)
28 .ok_or_else(|| anyhow!("unable to compute relative path to {path} from {base}"))
29 }
30
31 inner(path.as_ref(), base.as_ref())
32}
33
34pub fn path_relative_from_current_dir(path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
36 fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
37 let current_dir = std::env::current_dir()?;
38 path_relative_from(path, Utf8PathBuf::try_from(current_dir)?)
39 }
40 inner(path.as_ref())
41}
42
43pub fn path_relative_from_file(
47 path: impl AsRef<Utf8Path>,
48 file: impl AsRef<Utf8Path>,
49) -> Result<Utf8PathBuf> {
50 fn inner(path: &Utf8Path, file: &Utf8Path) -> Result<Utf8PathBuf> {
51 let base = file.parent().ok_or_else(|| {
52 anyhow!(
53 "The path to the file to be relative to does not appear to be the path to a file: {}",
54 file
55 )
56 })?;
57 path_relative_from(path, base)
58 }
59 inner(path.as_ref(), file.as_ref())
60}
61
62fn normalized_absolute_path(path: &Utf8Path) -> Result<Utf8PathBuf> {
63 if path.is_relative() {
64 let current_dir = std::env::current_dir()?;
65 normalize_path_impl(Utf8PathBuf::try_from(current_dir)?.join(path).components())
66 } else {
67 normalize_path_impl(path.components())
68 }
69}
70
71pub fn resolve_path_from_file(
87 path: impl AsRef<Utf8Path>,
88 resolve_from: impl AsRef<Utf8Path>,
89) -> Result<Utf8PathBuf> {
90 fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
91 let resolve_from_dir = resolve_from
92 .parent()
93 .with_context(|| format!("Not a path to a file: {resolve_from}"))?;
94 resolve_path(path, resolve_from_dir)
95 }
96 inner(path.as_ref(), resolve_from.as_ref())
97}
98pub fn resolve_path(
114 path: impl AsRef<Utf8Path>,
115 resolve_from: impl AsRef<Utf8Path>,
116) -> Result<Utf8PathBuf> {
117 fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
118 if path.is_absolute() {
119 Ok(path.to_owned())
120 } else {
121 normalize_path_impl(resolve_from.components().chain(path.components()))
122 .with_context(|| format!("resolving {} from {}", path, resolve_from))
123 }
124 }
125 inner(path.as_ref(), resolve_from.as_ref())
126}
127
128pub fn normalize_path(path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
133 fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
134 normalize_path_impl(path.components()).with_context(|| format!("Normalizing: {}", path))
135 }
136 inner(path.as_ref())
137}
138
139fn normalize_path_impl<'a>(
140 path_components: impl IntoIterator<Item = Utf8Component<'a>>,
141) -> Result<Utf8PathBuf> {
142 let result =
143 path_components.into_iter().try_fold(Vec::new(), |mut components, component| {
144 match component {
145 value @ Utf8Component::Normal(_) => components.push(value),
147 Utf8Component::CurDir => {}
149 Utf8Component::ParentDir => {
151 let popped = components.pop();
153 match popped {
154 None => components.push(Utf8Component::ParentDir),
156
157 Some(Utf8Component::Normal(_)) => {}
159
160 Some(value @ Utf8Component::ParentDir) => {
163 components.push(value);
164 components.push(component);
165 }
166 Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
169 return Err(anyhow!("Attempted to get parent of path root"))
170 }
171 Some(Utf8Component::CurDir) => unreachable!(),
173 }
174 }
175 abs_root @ Utf8Component::RootDir | abs_root @ Utf8Component::Prefix(_) => {
177 if components.is_empty() {
178 components.push(abs_root);
179 } else {
180 return Err(anyhow!(
181 "Encountered a path root that wasn't in the root position"
182 ));
183 }
184 }
185 }
186 Ok(components)
187 })?;
188 Ok(result.iter().collect())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn resolve_path_from_file_simple() {
197 let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap();
198 assert_eq!(result, Utf8PathBuf::from("path/to/an/internal/path"))
199 }
200
201 #[test]
202 fn resolve_path_from_file_fails_root() {
203 let result = resolve_path_from_file("an/internal/path", "/");
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn resolve_path_simple() {
209 let result = resolve_path("an/internal/path", "path/to/manifest_dir").unwrap();
210 assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
211 }
212
213 #[test]
214 fn resolve_path_with_abs_manifest_path_stays_abs() {
215 let result = resolve_path("an/internal/path", "/path/to/manifest_dir").unwrap();
216 assert_eq!(result, Utf8PathBuf::from("/path/to/manifest_dir/an/internal/path"))
217 }
218
219 #[test]
220 fn resolve_path_removes_cur_dirs() {
221 let result = resolve_path("./an/./internal/path", "./path/to/./manifest_dir").unwrap();
222 assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
223 }
224
225 #[test]
226 fn resolve_path_with_simple_parent_dirs() {
227 let result = resolve_path("../../an/internal/path", "path/to/manifest_dir").unwrap();
228 assert_eq!(result, Utf8PathBuf::from("path/an/internal/path"))
229 }
230
231 #[test]
232 fn resolve_path_with_parent_dirs_past_manifest_start() {
233 let result = resolve_path("../../../../an/internal/path", "path/to/manifest_dir").unwrap();
234 assert_eq!(result, Utf8PathBuf::from("../an/internal/path"))
235 }
236
237 #[test]
238 fn resolve_path_with_abs_internal_path() {
239 let result = resolve_path("/an/absolute/path", "path/to/manifest_dir").unwrap();
240 assert_eq!(result, Utf8PathBuf::from("/an/absolute/path"))
241 }
242
243 #[test]
244 fn resolve_path_fails_with_parent_dirs_past_abs_manifest() {
245 let result = resolve_path("../../../../an/internal/path", "/path/to/manifest_dir");
246 assert!(result.is_err())
247 }
248
249 #[test]
250 fn test_relative_from_absolute_when_already_relative() {
251 let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
252
253 let base = cwd.join("path/to/base/dir");
254 let path = "path/but/to/another/dir";
255
256 let relative_path = path_relative_from(path, base).unwrap();
257 assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
258 }
259
260 #[test]
261 fn test_relative_from_absolute_when_absolute() {
262 let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
263
264 let base = cwd.join("path/to/base/dir");
265 let path = cwd.join("path/but/to/another/dir");
266
267 let relative_path = path_relative_from(path, base).unwrap();
268 assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
269 }
270
271 #[test]
272 fn test_relative_from_relative_when_absolute_and_different_from_root() {
273 let base = "../some/relative/path";
274 let path = "/an/absolute/path";
275
276 let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
285
286 let expected_path = Utf8PathBuf::from_iter(
287 cwd.components()
288 .into_iter()
289 .filter_map(|comp| match comp {
290 Utf8Component::Normal(_) => Some(Utf8Component::ParentDir),
291 _ => None,
292 })
293 .skip(1),
297 )
298 .join("../../../")
300 .join("an/absolute/path");
302
303 let relative_path = path_relative_from(path, base).unwrap();
304 assert_eq!(relative_path, expected_path);
305 }
306
307 #[test]
308 fn test_relative_from_relative_when_absolute_and_shared_root_path() {
309 let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
310
311 let base = "some/relative/path";
312 let path = cwd.join("foo/bar");
313
314 let relative_path = path_relative_from(path, base).unwrap();
315 assert_eq!(relative_path, Utf8PathBuf::from("../../../foo/bar"));
316 }
317
318 #[test]
319 fn test_relative_from_relative_when_relative() {
320 let base = "some/relative/path";
321 let path = "another/relative/path";
322
323 let relative_path = path_relative_from(path, base).unwrap();
324 assert_eq!(relative_path, Utf8PathBuf::from("../../../another/relative/path"));
325 }
326
327 #[test]
328 fn test_relative_from_when_base_has_parent_component() {
329 assert_eq!(
330 path_relative_from("foo/bar", "baz/different_thing").unwrap(),
331 Utf8PathBuf::from("../../foo/bar")
332 );
333 assert_eq!(
334 path_relative_from("foo/bar", "baz/thing/../different_thing").unwrap(),
335 Utf8PathBuf::from("../../foo/bar")
336 );
337 }
338
339 #[test]
340 fn test_relative_from_file_simple() {
341 let file = "some/path/to/file.txt";
342 let path = "some/path/to/data/file";
343
344 let relative_path = path_relative_from_file(path, file).unwrap();
345 assert_eq!(relative_path, Utf8PathBuf::from("data/file"));
346 }
347
348 #[test]
349 fn test_relative_from_file_when_file_not_a_file() {
350 let file = "/";
351 let path = "some/path/to/data/file";
352
353 let relative_path = path_relative_from_file(path, file);
354 assert!(relative_path.is_err());
355 }
356
357 #[test]
358 fn test_relative_from_current_dir() {
359 let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
360
361 let base = "some/relative/path";
362 let path = cwd.join(base);
363
364 let relative_path = path_relative_from_current_dir(path).unwrap();
365 assert_eq!(relative_path, Utf8PathBuf::from(base));
366 }
367}