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.
45use anyhow::{anyhow, Context, Result};
6use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
7use pathdiff::diff_utf8_paths;
89/// 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> {
20fn inner(path: &Utf8Path, base: &Utf8Path) -> Result<Utf8PathBuf> {
21let path = normalized_absolute_path(path)
22 .with_context(|| format!("converting path to normalized absolute path: {path}"))?;
2324let base = normalized_absolute_path(base)
25 .with_context(|| format!("converting base to normalized absolute path: {base}"))?;
2627 diff_utf8_paths(&path, &base)
28 .ok_or_else(|| anyhow!("unable to compute relative path to {path} from {base}"))
29 }
3031 inner(path.as_ref(), base.as_ref())
32}
3334/// 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> {
36fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
37let current_dir = std::env::current_dir()?;
38 path_relative_from(path, Utf8PathBuf::try_from(current_dir)?)
39 }
40 inner(path.as_ref())
41}
4243/// 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> {
50fn inner(path: &Utf8Path, file: &Utf8Path) -> Result<Utf8PathBuf> {
51let base = file.parent().ok_or_else(|| {
52anyhow!(
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}
6162fn normalized_absolute_path(path: &Utf8Path) -> Result<Utf8PathBuf> {
63if path.is_relative() {
64let 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}
7071/// 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> {
90fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
91let 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> {
117fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
118if path.is_absolute() {
119Ok(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}
127128/// 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> {
133fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
134 normalize_path_impl(path.components()).with_context(|| format!("Normalizing: {}", path))
135 }
136 inner(path.as_ref())
137}
138139fn normalize_path_impl<'a>(
140 path_components: impl IntoIterator<Item = Utf8Component<'a>>,
141) -> Result<Utf8PathBuf> {
142let result =
143 path_components.into_iter().try_fold(Vec::new(), |mut components, component| {
144match component {
145// accumulate normal segments.
146value @ Utf8Component::Normal(_) => components.push(value),
147// Drop current directory segments.
148Utf8Component::CurDir => {}
149// Parent dir segments require special handling
150Utf8Component::ParentDir => {
151// Inspect the last item in the acculuated path
152let popped = components.pop();
153match popped {
154// acculator is empty, so just append the parent.
155None => components.push(Utf8Component::ParentDir),
156157// If the last item was normal, then drop it.
158Some(Utf8Component::Normal(_)) => {}
159160// The last item was a parent, and this is a parent, so push
161 // them BOTH onto the stack (we're going deeper).
162Some(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.
168Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
169return Err(anyhow!("Attempted to get parent of path root"))
170 }
171// Never pushed to stack, can't happen.
172Some(Utf8Component::CurDir) => unreachable!(),
173 }
174 }
175// absolute path roots get pushed onto the stack, but only if empty.
176abs_root @ Utf8Component::RootDir | abs_root @ Utf8Component::Prefix(_) => {
177if components.is_empty() {
178 components.push(abs_root);
179 } else {
180return Err(anyhow!(
181"Encountered a path root that wasn't in the root position"
182));
183 }
184 }
185 }
186Ok(components)
187 })?;
188Ok(result.iter().collect())
189}
190191#[cfg(test)]
192mod tests {
193use super::*;
194195#[test]
196fn resolve_path_from_file_simple() {
197let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap();
198assert_eq!(result, Utf8PathBuf::from("path/to/an/internal/path"))
199 }
200201#[test]
202fn resolve_path_from_file_fails_root() {
203let result = resolve_path_from_file("an/internal/path", "/");
204assert!(result.is_err());
205 }
206207#[test]
208fn resolve_path_simple() {
209let result = resolve_path("an/internal/path", "path/to/manifest_dir").unwrap();
210assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
211 }
212213#[test]
214fn resolve_path_with_abs_manifest_path_stays_abs() {
215let result = resolve_path("an/internal/path", "/path/to/manifest_dir").unwrap();
216assert_eq!(result, Utf8PathBuf::from("/path/to/manifest_dir/an/internal/path"))
217 }
218219#[test]
220fn resolve_path_removes_cur_dirs() {
221let result = resolve_path("./an/./internal/path", "./path/to/./manifest_dir").unwrap();
222assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
223 }
224225#[test]
226fn resolve_path_with_simple_parent_dirs() {
227let result = resolve_path("../../an/internal/path", "path/to/manifest_dir").unwrap();
228assert_eq!(result, Utf8PathBuf::from("path/an/internal/path"))
229 }
230231#[test]
232fn resolve_path_with_parent_dirs_past_manifest_start() {
233let result = resolve_path("../../../../an/internal/path", "path/to/manifest_dir").unwrap();
234assert_eq!(result, Utf8PathBuf::from("../an/internal/path"))
235 }
236237#[test]
238fn resolve_path_with_abs_internal_path() {
239let result = resolve_path("/an/absolute/path", "path/to/manifest_dir").unwrap();
240assert_eq!(result, Utf8PathBuf::from("/an/absolute/path"))
241 }
242243#[test]
244fn resolve_path_fails_with_parent_dirs_past_abs_manifest() {
245let result = resolve_path("../../../../an/internal/path", "/path/to/manifest_dir");
246assert!(result.is_err())
247 }
248249#[test]
250fn test_relative_from_absolute_when_already_relative() {
251let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
252253let base = cwd.join("path/to/base/dir");
254let path = "path/but/to/another/dir";
255256let relative_path = path_relative_from(path, base).unwrap();
257assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
258 }
259260#[test]
261fn test_relative_from_absolute_when_absolute() {
262let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
263264let base = cwd.join("path/to/base/dir");
265let path = cwd.join("path/but/to/another/dir");
266267let relative_path = path_relative_from(path, base).unwrap();
268assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
269 }
270271#[test]
272fn test_relative_from_relative_when_absolute_and_different_from_root() {
273let base = "../some/relative/path";
274let path = "/an/absolute/path";
275276// 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.
283284let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
285286let 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");
302303let relative_path = path_relative_from(path, base).unwrap();
304assert_eq!(relative_path, expected_path);
305 }
306307#[test]
308fn test_relative_from_relative_when_absolute_and_shared_root_path() {
309let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
310311let base = "some/relative/path";
312let path = cwd.join("foo/bar");
313314let relative_path = path_relative_from(path, base).unwrap();
315assert_eq!(relative_path, Utf8PathBuf::from("../../../foo/bar"));
316 }
317318#[test]
319fn test_relative_from_relative_when_relative() {
320let base = "some/relative/path";
321let path = "another/relative/path";
322323let relative_path = path_relative_from(path, base).unwrap();
324assert_eq!(relative_path, Utf8PathBuf::from("../../../another/relative/path"));
325 }
326327#[test]
328fn test_relative_from_when_base_has_parent_component() {
329assert_eq!(
330 path_relative_from("foo/bar", "baz/different_thing").unwrap(),
331 Utf8PathBuf::from("../../foo/bar")
332 );
333assert_eq!(
334 path_relative_from("foo/bar", "baz/thing/../different_thing").unwrap(),
335 Utf8PathBuf::from("../../foo/bar")
336 );
337 }
338339#[test]
340fn test_relative_from_file_simple() {
341let file = "some/path/to/file.txt";
342let path = "some/path/to/data/file";
343344let relative_path = path_relative_from_file(path, file).unwrap();
345assert_eq!(relative_path, Utf8PathBuf::from("data/file"));
346 }
347348#[test]
349fn test_relative_from_file_when_file_not_a_file() {
350let file = "/";
351let path = "some/path/to/data/file";
352353let relative_path = path_relative_from_file(path, file);
354assert!(relative_path.is_err());
355 }
356357#[test]
358fn test_relative_from_current_dir() {
359let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
360361let base = "some/relative/path";
362let path = cwd.join(base);
363364let relative_path = path_relative_from_current_dir(path).unwrap();
365assert_eq!(relative_path, Utf8PathBuf::from(base));
366 }
367}