| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{anyhow, Context, Result}; |
| use camino::Utf8PathBuf; |
| use pathdiff::diff_paths; |
| use serde::{Deserialize, Serialize}; |
| use std::{ |
| hash::Hash, |
| marker::PhantomData, |
| path::{Component, Path, PathBuf}, |
| }; |
| |
| /// A base trait for TypePath's marker traits. |
| pub trait PathTypeMarker { |
| /// A reference to an object that implements Display, and gives the |
| /// displayable semantic type for this path. This is used by the Debug |
| /// implementation of `TypedPathBuf` to display the semantic type for the |
| /// path: |
| /// |
| /// ``` |
| /// struct MarkerStructType; |
| /// impl_path_type_marker!(MarkerStructType); |
| /// |
| /// let typed_path = TypedPathBuf<MarkerStructType>::from("some/path"); |
| /// println!("{:?}", typed_path); |
| /// ``` |
| /// will print: |
| /// |
| /// ```text |
| /// TypedPathBuf<MarkerStructType>("some/path") |
| /// ``` |
| fn path_type_display() -> &'static dyn std::fmt::Display; |
| } |
| |
| /// Implement the `PathTypeMarker` trait for a given marker-type struct. This |
| /// mainly simplifies the creation of a display-string for the type. |
| #[macro_export] |
| macro_rules! impl_path_type_marker { |
| // This macro takes an argument of the marker struct's type name, and then |
| // provides an implementation of 'PathTypeMarker' for it. |
| ($struct_name:ident) => { |
| impl PathTypeMarker for $struct_name { |
| fn path_type_display() -> &'static dyn std::fmt::Display { |
| &stringify!($struct_name) |
| } |
| } |
| }; |
| } |
| |
| /// A path, in valid utf-8, which carries a marker for what kind of path it is. |
| #[derive(Clone, Serialize, Deserialize)] |
| #[repr(transparent)] |
| #[serde(transparent)] |
| pub struct TypedPathBuf<P: PathTypeMarker> { |
| #[serde(flatten)] |
| inner: Utf8PathBuf, |
| |
| #[serde(skip)] |
| _marker: PhantomData<P>, |
| } |
| |
| /// This derefs into the typed version of utf8 path, not utf8 path itself, so |
| /// that it is easier to use in typed contexts, and makes the switchover to |
| /// a non-typed context more explicit. |
| /// |
| /// This also causes any path manipulations (join, etc.) to be done without the |
| /// semantic type, so that the caller has to be explicit that it's still the |
| /// semantic type (using 'into()', for instance). |
| impl<P: PathTypeMarker> std::ops::Deref for TypedPathBuf<P> { |
| type Target = Utf8PathBuf; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.inner |
| } |
| } |
| |
| impl<P: PathTypeMarker> TypedPathBuf<P> { |
| /// Convert this TypedPathBuf into a standard (OsStr-based) `PathBuf`. This |
| /// both strips it of semantic type and that it's known to be Utf-8. |
| pub fn into_std_path_buf(self) -> PathBuf { |
| self.inner.into_std_path_buf() |
| } |
| } |
| |
| /// The Debug implementation displays like a type-struct that carries the marker |
| /// type for the path: |
| /// |
| /// ```text |
| /// TypedPathBuf<MarkerStructType>("some/path") |
| /// ``` |
| impl<P: PathTypeMarker> std::fmt::Debug for TypedPathBuf<P> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_tuple(&format!("TypedPathBuf<{}>", P::path_type_display())) |
| .field(&self.inner.to_string()) |
| .finish() |
| } |
| } |
| |
| /// The Display implementation defers to the wrapped path. |
| impl<P: PathTypeMarker> std::fmt::Display for TypedPathBuf<P> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| self.inner.fmt(f) |
| } |
| } |
| |
| /// Implement From<> for path-like sources. Note that these also will infer the |
| /// semantic type, which while useful in some contexts, can cause issues in |
| /// places where multiple different type markers are used: |
| /// |
| /// ``` |
| /// fn some_func(source: TypedPathBuf<Source>, TypedPathBuf<Destination>); |
| /// |
| /// // This infers the types of the paths: |
| /// some_func("source_path".into(), "destination_path".into()); |
| /// |
| /// // allowing this error: |
| /// some_func("destination_path".into(), "source_path",into()); |
| /// |
| /// // In these cases, it's best to strongly type one or both of them: |
| /// some_func(TypedPathBuf<Source>::from("source_path"), "destination_path".into()); |
| /// |
| /// // or (better) |
| /// some_func(TypedPathBuf<Source>::from("source_path"), |
| /// TypedPathBuf<Destination>::from("destination_path")); |
| /// ``` |
| // inner module used to group impls and to add above documentation. |
| mod from_impls { |
| use super::*; |
| |
| impl<P: PathTypeMarker> From<Utf8PathBuf> for TypedPathBuf<P> { |
| fn from(path: Utf8PathBuf) -> Self { |
| Self { inner: path, _marker: PhantomData } |
| } |
| } |
| |
| impl<P: PathTypeMarker> From<String> for TypedPathBuf<P> { |
| fn from(s: String) -> TypedPathBuf<P> { |
| TypedPathBuf::from(Utf8PathBuf::from(s)) |
| } |
| } |
| |
| impl<P: PathTypeMarker> From<&str> for TypedPathBuf<P> { |
| fn from(s: &str) -> TypedPathBuf<P> { |
| TypedPathBuf::from(Utf8PathBuf::from(s)) |
| } |
| } |
| |
| impl<P: PathTypeMarker> std::str::FromStr for TypedPathBuf<P> { |
| type Err = String; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| Ok(Self::from(s)) |
| } |
| } |
| } |
| |
| // These comparison implementations are required because #[derive(...)] will not |
| // derive these if `P` doesn't implement them, but `P` has no reason to |
| // implement them, so these implementations just pass through to the Utf8PathBuf |
| // implementations. |
| |
| impl<P: PathTypeMarker> PartialOrd for TypedPathBuf<P> { |
| fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
| self.inner.partial_cmp(&other.inner) |
| } |
| } |
| |
| impl<P: PathTypeMarker> Ord for TypedPathBuf<P> { |
| fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
| self.inner.cmp(&other.inner) |
| } |
| } |
| |
| impl<P: PathTypeMarker> PartialEq for TypedPathBuf<P> { |
| fn eq(&self, other: &Self) -> bool { |
| self.inner == other.inner |
| } |
| } |
| |
| impl<P: PathTypeMarker> Eq for TypedPathBuf<P> {} |
| |
| impl<P: PathTypeMarker> Hash for TypedPathBuf<P> { |
| fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
| self.inner.hash(state); |
| } |
| } |
| |
| /// Helper to make one path relative to a directory. |
| /// |
| /// This is similar to GN's `rebase_path(path, new_base)`. |
| /// |
| /// To do the calculation, both 'path' and 'base' are made absolute, using the |
| /// current working dir as the basis for converting a relative path to absolute, |
| /// and then the relative path from one to the other is computed. |
| pub fn path_relative_from(path: impl AsRef<Path>, base: impl AsRef<Path>) -> Result<PathBuf> { |
| let path = normalized_absolute_path(&path).with_context(|| { |
| format!("converting path to normalized absolute path: {}", path.as_ref().display()) |
| })?; |
| let base = normalized_absolute_path(&base).with_context(|| { |
| format!("converting base to normalized absolute path: {}", base.as_ref().display()) |
| })?; |
| |
| diff_paths(&path, &base).ok_or_else(|| { |
| anyhow!("unable to compute relative path to {} from {}", path.display(), base.display()) |
| }) |
| } |
| |
| /// Helper to make a path relative to the path to a file. This is the same as |
| /// [path_relative_from(file.parent()?)] |
| /// |
| pub fn path_relative_from_file(path: impl AsRef<Path>, file: impl AsRef<Path>) -> Result<PathBuf> { |
| let file = file.as_ref(); |
| let base = file.parent().ok_or_else(|| { |
| anyhow!( |
| "The path to the file to be relative to does not appear to be the path to a file: {}", |
| file.display() |
| ) |
| })?; |
| path_relative_from(path, base) |
| } |
| |
| fn normalized_absolute_path(path: impl AsRef<Path>) -> Result<PathBuf> { |
| let path = path.as_ref(); |
| if path.is_relative() { |
| normalize_path_impl(std::env::current_dir()?.join(path).components()) |
| } else { |
| normalize_path_impl(path.components()) |
| } |
| } |
| |
| /// Helper to resolve a path that's relative to some other path into a |
| /// normalized path. |
| /// |
| /// # Example |
| /// |
| /// a file at: `some/path/to/a/manifest.txt` |
| /// contains within it the path: `../some/internal/path`. |
| /// |
| /// ``` |
| /// use assembly_util::path_to_string::resolve_path; |
| /// |
| /// let rebased = resolve_path("../some/internal/path", "some/path/to/some/manifest.txt") |
| /// assert_eq!(rebased.unwrap(), "some/path/to/some/internal/path") |
| /// ``` |
| /// |
| pub fn resolve_path_from_file( |
| path: impl AsRef<Path>, |
| resolve_from: impl AsRef<Path>, |
| ) -> Result<PathBuf> { |
| let resolve_from = resolve_from.as_ref(); |
| let resolve_from_dir = resolve_from |
| .parent() |
| .with_context(|| format!("Not a path to a file: {}", resolve_from.display()))?; |
| resolve_path(path, resolve_from_dir) |
| } |
| /// Helper to resolve a path that's relative to some other path into a |
| /// normalized path. |
| /// |
| /// # Example |
| /// |
| /// a file at: `some/path/to/some/manifest_dir/some_file.txt` |
| /// contains within it the path: `../some/internal/path`. |
| /// |
| /// ``` |
| /// use assembly_util::path_to_string::resolve_path; |
| /// |
| /// let rebased = resolve_path("../some/internal/path", "some/path/to/some/manifest_dir/") |
| /// assert_eq!(rebased.unwrap(), "some/path/to/some/internal/path") |
| /// ``` |
| /// |
| pub fn resolve_path(path: impl AsRef<Path>, resolve_from: impl AsRef<Path>) -> Result<PathBuf> { |
| let path = path.as_ref(); |
| let resolve_from = resolve_from.as_ref(); |
| if path.is_absolute() { |
| Ok(path.to_owned()) |
| } else { |
| normalize_path_impl(resolve_from.components().chain(path.components())).with_context(|| { |
| format!("resolving {} from {}", path.display(), resolve_from.display()) |
| }) |
| } |
| } |
| |
| /// Given a path with internal `.` and `..`, normalize out those path segments. |
| /// |
| /// This does not consult the filesystem to follow symlinks, it only operates |
| /// on the path components themselves. |
| pub fn normalize_path(path: impl AsRef<Path>) -> Result<PathBuf> { |
| normalize_path_impl(path.as_ref().components()) |
| .with_context(|| format!("Normalizing: {}", path.as_ref().display())) |
| } |
| |
| fn normalize_path_impl<'a>( |
| path_components: impl IntoIterator<Item = Component<'a>>, |
| ) -> Result<PathBuf> { |
| let result = |
| path_components.into_iter().try_fold(Vec::new(), |mut components, component| { |
| match component { |
| // accumulate normal segments. |
| value @ Component::Normal(_) => components.push(value), |
| // Drop current directory segments. |
| Component::CurDir => {} |
| // Parent dir segments require special handling |
| Component::ParentDir => { |
| // Inspect the last item in the acculuated path |
| let popped = components.pop(); |
| match popped { |
| // acculator is empty, so just append the parent. |
| None => components.push(Component::ParentDir), |
| |
| // If the last item was normal, then drop it. |
| Some(Component::Normal(_)) => {} |
| |
| // The last item was a parent, and this is a parent, so push |
| // them BOTH onto the stack (we're going deeper). |
| Some(value @ Component::ParentDir) => { |
| components.push(value); |
| components.push(component); |
| } |
| // If the last item in the stack is an absolute path root |
| // then fail. |
| Some(Component::RootDir) | Some(Component::Prefix(_)) => { |
| return Err(anyhow!("Attempted to get parent of path root")) |
| } |
| // Never pushed to stack, can't happen. |
| Some(Component::CurDir) => unreachable!(), |
| } |
| } |
| // absolute path roots get pushed onto the stack, but only if empty. |
| abs_root @ Component::RootDir | abs_root @ Component::Prefix(_) => { |
| if components.is_empty() { |
| components.push(abs_root); |
| } else { |
| return Err(anyhow!( |
| "Encountered a path root that wasn't in the root position" |
| )); |
| } |
| } |
| } |
| Ok(components) |
| })?; |
| Ok(result.iter().collect()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::str::FromStr; |
| use std::{iter::FromIterator, path::PathBuf}; |
| |
| struct TestPathType {} |
| impl_path_type_marker!(TestPathType); |
| |
| #[test] |
| fn make_typed_path_from_string() { |
| let original: String = "/this/is/a/string".to_string(); |
| let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap(); |
| assert_eq!(typed.to_string(), original); |
| } |
| |
| #[test] |
| fn make_typed_path_from_str() { |
| let original: &str = "/this/is/a/string"; |
| let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap(); |
| assert_eq!(typed.to_string(), original); |
| } |
| |
| #[test] |
| fn path_type_deserialization() { |
| #[derive(Debug, Deserialize)] |
| struct Sample { |
| pub path: TypedPathBuf<TestPathType>, |
| } |
| let parsed: Sample = serde_json::from_str("{ \"path\": \"this/is/a/path\"}").unwrap(); |
| assert_eq!(parsed.path, TypedPathBuf::<TestPathType>::from("this/is/a/path")); |
| } |
| |
| #[test] |
| fn path_type_serialization() { |
| #[derive(Debug, Serialize)] |
| struct Sample { |
| pub path: TypedPathBuf<TestPathType>, |
| } |
| let sample = Sample { path: "this/is/a/path".into() }; |
| let expected = serde_json::json!({ "path": "this/is/a/path"}); |
| assert_eq!(serde_json::to_value(sample).unwrap(), expected); |
| } |
| |
| #[test] |
| fn typed_path_debug_impl() { |
| let typed = TypedPathBuf::<TestPathType>::from("some/path"); |
| assert_eq!(format!("{:?}", typed), "TypedPathBuf<TestPathType>(\"some/path\")"); |
| } |
| |
| #[test] |
| fn typed_path_display_impl() { |
| let typed = TypedPathBuf::<TestPathType>::from("some/path"); |
| assert_eq!(format!("{}", typed), "some/path"); |
| } |
| |
| #[test] |
| fn typed_path_buf_into_path_buf() { |
| let typed = TypedPathBuf::<TestPathType>::from("some/path"); |
| assert_eq!(typed.into_std_path_buf(), PathBuf::from("some/path")); |
| } |
| |
| #[test] |
| fn typed_path_derefs_into_utf8_path() { |
| let typed = TypedPathBuf::<TestPathType>::from("some/path"); |
| let utf8_path = Utf8PathBuf::from("some/path"); |
| assert_eq!(*typed, utf8_path); |
| } |
| |
| #[test] |
| fn resolve_path_from_file_simple() { |
| let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap(); |
| assert_eq!(result, PathBuf::from("path/to/an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_from_file_fails_root() { |
| let result = resolve_path_from_file("an/internal/path", "/"); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn resolve_path_simple() { |
| let result = resolve_path("an/internal/path", "path/to/manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("path/to/manifest_dir/an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_with_abs_manifest_path_stays_abs() { |
| let result = resolve_path("an/internal/path", "/path/to/manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("/path/to/manifest_dir/an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_removes_cur_dirs() { |
| let result = resolve_path("./an/./internal/path", "./path/to/./manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("path/to/manifest_dir/an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_with_simple_parent_dirs() { |
| let result = resolve_path("../../an/internal/path", "path/to/manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("path/an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_with_parent_dirs_past_manifest_start() { |
| let result = resolve_path("../../../../an/internal/path", "path/to/manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("../an/internal/path")) |
| } |
| |
| #[test] |
| fn resolve_path_with_abs_internal_path() { |
| let result = resolve_path("/an/absolute/path", "path/to/manifest_dir").unwrap(); |
| assert_eq!(result, PathBuf::from("/an/absolute/path")) |
| } |
| |
| #[test] |
| fn resolve_path_fails_with_parent_dirs_past_abs_manifest() { |
| let result = resolve_path("../../../../an/internal/path", "/path/to/manifest_dir"); |
| assert!(result.is_err()) |
| } |
| |
| #[test] |
| fn test_relative_from_absolute_when_already_relative() { |
| let cwd = std::env::current_dir().unwrap(); |
| |
| let base = cwd.join("path/to/base/dir"); |
| let path = "path/but/to/another/dir"; |
| |
| let relative_path = path_relative_from(path, base).unwrap(); |
| assert_eq!(relative_path, PathBuf::from("../../../but/to/another/dir")); |
| } |
| |
| #[test] |
| fn test_relative_from_absolute_when_absolute() { |
| let cwd = std::env::current_dir().unwrap(); |
| |
| let base = cwd.join("path/to/base/dir"); |
| let path = cwd.join("path/but/to/another/dir"); |
| |
| let relative_path = path_relative_from(path, base).unwrap(); |
| assert_eq!(relative_path, PathBuf::from("../../../but/to/another/dir")); |
| } |
| |
| #[test] |
| fn test_relative_from_relative_when_absolute_and_different_from_root() { |
| let base = "../some/relative/path"; |
| let path = "/an/absolute/path"; |
| |
| // The relative path to an absolute path from a relative base (relative |
| // to cwd), is the number of ParendDir components needed to reach the |
| // root, and then the absolute path itself. It's only this long when |
| // the paths have nothing in common from the root. |
| // |
| // To compute this path, we need to convert the "normal" segments of the |
| // cwd path into ParentDir ("..") components. |
| |
| let cwd = std::env::current_dir().unwrap(); |
| let expected_path = PathBuf::from_iter( |
| cwd.components() |
| .into_iter() |
| .filter_map(|comp| match comp { |
| Component::Normal(_) => Some(Component::ParentDir), |
| _ => None, |
| }) |
| // Skip one of the '..' segments, because the 'base' we are |
| // using in this test starts with a '..', and normalizing |
| // cwd.join(base) will remove the last component from cwd. |
| .skip(1), |
| ) |
| // Join that path with 'some/relative/path' converted to '..' segments |
| .join("../../../") |
| // And join that path with the 'path' itself. |
| .join("an/absolute/path"); |
| |
| let relative_path = path_relative_from(path, base).unwrap(); |
| assert_eq!(relative_path, expected_path); |
| } |
| |
| #[test] |
| fn test_relative_from_relative_when_absolute_and_shared_root_path() { |
| let cwd = std::env::current_dir().unwrap(); |
| |
| let base = "some/relative/path"; |
| let path = cwd.join("foo/bar"); |
| |
| let relative_path = path_relative_from(path, base).unwrap(); |
| assert_eq!(relative_path, PathBuf::from("../../../foo/bar")); |
| } |
| |
| #[test] |
| fn test_relative_from_relative_when_relative() { |
| let base = "some/relative/path"; |
| let path = "another/relative/path"; |
| |
| let relative_path = path_relative_from(path, base).unwrap(); |
| assert_eq!(relative_path, PathBuf::from("../../../another/relative/path")); |
| } |
| |
| #[test] |
| fn test_relative_from_when_base_has_parent_component() { |
| assert_eq!( |
| path_relative_from("foo/bar", "baz/different_thing").unwrap(), |
| PathBuf::from("../../foo/bar") |
| ); |
| assert_eq!( |
| path_relative_from("foo/bar", "baz/thing/../different_thing").unwrap(), |
| PathBuf::from("../../foo/bar") |
| ); |
| } |
| |
| #[test] |
| fn test_relative_from_file_simple() { |
| let file = "some/path/to/file.txt"; |
| let path = "some/path/to/data/file"; |
| |
| let relative_path = path_relative_from_file(path, file).unwrap(); |
| assert_eq!(relative_path, PathBuf::from("data/file")); |
| } |
| |
| #[test] |
| fn test_relative_from_file_when_file_not_a_file() { |
| let file = "/"; |
| let path = "some/path/to/data/file"; |
| |
| let relative_path = path_relative_from_file(path, file); |
| assert!(relative_path.is_err()); |
| } |
| } |