utf8_path/
paths.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{anyhow, Context, Result};
6use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
7use pathdiff::diff_utf8_paths;
8
9/// Helper to make one path relative to a directory.
10///
11/// This is similar to GN's `rebase_path(path, new_base)`.
12///
13/// To do the calculation, both 'path' and 'base' are made absolute, using the
14/// current working dir as the basis for converting a relative path to absolute,
15/// and then the relative path from one to the other is computed.
16pub 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
34/// Helper to convert an absolute path into a path relative to the current directory
35pub 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
43/// Helper to make a path relative to the path to a file.  This is the same as
44/// [path_relative_from(file.parent()?)]
45///
46pub 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
71/// Helper to resolve a path that's relative to some other path into a
72/// normalized path.
73///
74/// # Example
75///
76/// a file at: `some/path/to/a/manifest.txt`
77/// contains within it the path: `../some/internal/path`.
78///
79/// ```
80///   use utf8_path::path_to_string::resolve_path;
81///
82///   let rebased = resolve_path("../some/internal/path", "some/path/to/some/manifest.txt")
83///   assert_eq!(rebased.unwrap(), "some/path/to/some/internal/path")
84/// ```
85///
86pub 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}
98/// Helper to resolve a path that's relative to some other path into a
99/// normalized path.
100///
101/// # Example
102///
103/// a file at: `some/path/to/some/manifest_dir/some_file.txt`
104/// contains within it the path: `../some/internal/path`.
105///
106/// ```
107///   use utf8_path::path_to_string::resolve_path;
108///
109///   let rebased = resolve_path("../some/internal/path", "some/path/to/some/manifest_dir/")
110///   assert_eq!(rebased.unwrap(), "some/path/to/some/internal/path")
111/// ```
112///
113pub 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
128/// Given a path with internal `.` and `..`, normalize out those path segments.
129///
130/// This does not consult the filesystem to follow symlinks, it only operates
131/// on the path components themselves.
132pub 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                // accumulate normal segments.
146                value @ Utf8Component::Normal(_) => components.push(value),
147                // Drop current directory segments.
148                Utf8Component::CurDir => {}
149                // Parent dir segments require special handling
150                Utf8Component::ParentDir => {
151                    // Inspect the last item in the acculuated path
152                    let popped = components.pop();
153                    match popped {
154                        // acculator is empty, so just append the parent.
155                        None => components.push(Utf8Component::ParentDir),
156
157                        // If the last item was normal, then drop it.
158                        Some(Utf8Component::Normal(_)) => {}
159
160                        // The last item was a parent, and this is a parent, so push
161                        // them BOTH onto the stack (we're going deeper).
162                        Some(value @ Utf8Component::ParentDir) => {
163                            components.push(value);
164                            components.push(component);
165                        }
166                        // If the last item in the stack is an absolute path root
167                        // then fail.
168                        Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
169                            return Err(anyhow!("Attempted to get parent of path root"))
170                        }
171                        // Never pushed to stack, can't happen.
172                        Some(Utf8Component::CurDir) => unreachable!(),
173                    }
174                }
175                // absolute path roots get pushed onto the stack, but only if empty.
176                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        // The relative path to an absolute path from a relative base (relative
277        // to cwd), is the number of ParendDir components needed to reach the
278        // root, and then the absolute path itself.  It's only this long when
279        // the paths have nothing in common from the root.
280        //
281        // To compute this path, we need to convert the "normal" segments of the
282        // cwd path into ParentDir ("..") components.
283
284        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 one of the '..' segments, because the 'base' we are
294                // using in this test starts with a '..', and normalizing
295                // cwd.join(base) will remove the last component from cwd.
296                .skip(1),
297        )
298        // Join that path with 'some/relative/path' converted to '..' segments
299        .join("../../../")
300        // And join that path with the 'path' itself.
301        .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}