Add typed path forms (#13115)

# Description
This PR adds new types to `nu-path` to enforce path invariants. Namely,
this PR adds:
- `Path` and `PathBuf`. These types are different from, but analogous to
`std::path::Path` and `std::path::PathBuf`.
- `RelativePath` and `RelativePathBuf`. These types must be/contain
strictly relative paths.
- `AbsolutePath` and `AbsolutePathBuf`. These types must be/contain
strictly absolute paths.
- `CanonicalPath` and `CanonicalPathBuf`. These types must be/contain
canonical paths.

Operations are prohibited as necessary to ensure that the invariants of
each type are upheld (needs double-checking).

Only paths that are absolute (or canonical) can be easily used as /
converted to `std::path::Path`s. This is to help force us to account for
the emulated current working directory instead of accidentally using the
current directory of the Nushell process (i.e.,
`std::env::current_dir`). Related to #12975 and #12976.

Note that this PR uses several declarative macros, as the file / this PR
would otherwise be 5000 lines long.

# User-Facing Changes
No major changes yet, just adds types to `nu-path` to be used in the
future.

# After Submitting
Actually use the new path types in all our crates where it makes sense,
removing usages of `std::path` types.
This commit is contained in:
Ian Manske 2024-06-26 01:33:57 +00:00 committed by GitHub
parent def36865ef
commit 5a486029db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 3260 additions and 0 deletions

161
crates/nu-path/src/form.rs Normal file
View file

@ -0,0 +1,161 @@
use std::ffi::OsStr;
mod private {
use std::ffi::OsStr;
// This trait should not be extended by external crates in order to uphold safety guarantees.
// As such, this trait is put inside a private module to prevent external impls.
// This ensures that all possible [`PathForm`]s can only be defined here and will:
// - be zero sized (enforced anyways by the `repr(transparent)` on `Path`)
// - have a no-op [`Drop`] implementation
pub trait Sealed: 'static {
fn invariants_satisfied<P: AsRef<OsStr> + ?Sized>(path: &P) -> bool;
}
}
/// A marker trait for the different kinds of path forms.
/// Each form has its own invariants that are guaranteed be upheld.
/// The list of path forms are:
/// - [`Any`]: a path with no invariants. It may be a relative or an absolute path.
/// - [`Relative`]: a strictly relative path.
/// - [`Absolute`]: a strictly absolute path.
/// - [`Canonical`]: a path that must be in canonicalized form.
pub trait PathForm: private::Sealed {}
impl PathForm for Any {}
impl PathForm for Relative {}
impl PathForm for Absolute {}
impl PathForm for Canonical {}
/// A path whose form is unknown. It could be a relative, absolute, or canonical path.
///
/// The path is not guaranteed to be normalized. It may contain unresolved symlinks,
/// trailing slashes, dot components (`..` or `.`), and repeated path separators.
pub struct Any;
impl private::Sealed for Any {
fn invariants_satisfied<P: AsRef<OsStr> + ?Sized>(_: &P) -> bool {
true
}
}
/// A strictly relative path.
///
/// The path is not guaranteed to be normalized. It may contain unresolved symlinks,
/// trailing slashes, dot components (`..` or `.`), and repeated path separators.
pub struct Relative;
impl private::Sealed for Relative {
fn invariants_satisfied<P: AsRef<OsStr> + ?Sized>(path: &P) -> bool {
std::path::Path::new(path).is_relative()
}
}
/// An absolute path.
///
/// The path is not guaranteed to be normalized. It may contain unresolved symlinks,
/// trailing slashes, dot components (`..` or `.`), and repeated path separators.
pub struct Absolute;
impl private::Sealed for Absolute {
fn invariants_satisfied<P: AsRef<OsStr> + ?Sized>(path: &P) -> bool {
std::path::Path::new(path).is_absolute()
}
}
// A canonical path.
//
// An absolute path with all intermediate components normalized and symbolic links resolved.
pub struct Canonical;
impl private::Sealed for Canonical {
fn invariants_satisfied<P: AsRef<OsStr> + ?Sized>(_: &P) -> bool {
true
}
}
/// A marker trait for [`PathForm`]s that may be relative paths.
/// This includes only the [`Any`] and [`Relative`] path forms.
///
/// [`push`](crate::PathBuf::push) and [`join`](crate::Path::join)
/// operations only support [`MaybeRelative`] path forms as input.
pub trait MaybeRelative: PathForm {}
impl MaybeRelative for Any {}
impl MaybeRelative for Relative {}
/// A marker trait for [`PathForm`]s that may be absolute paths.
/// This includes the [`Any`], [`Absolute`], and [`Canonical`] path forms.
pub trait MaybeAbsolute: PathForm {}
impl MaybeAbsolute for Any {}
impl MaybeAbsolute for Absolute {}
impl MaybeAbsolute for Canonical {}
/// A marker trait for [`PathForm`]s that are absolute paths.
/// This includes only the [`Absolute`] and [`Canonical`] path forms.
///
/// Only [`PathForm`]s that implement this trait can be easily converted to [`std::path::Path`]
/// or [`std::path::PathBuf`]. This is to encourage/force other Nushell crates to account for
/// the emulated current working directory, instead of using the [`std::env::current_dir`].
pub trait IsAbsolute: PathForm {}
impl IsAbsolute for Absolute {}
impl IsAbsolute for Canonical {}
/// A marker trait that signifies one [`PathForm`] can be used as or trivially converted to
/// another [`PathForm`].
///
/// The list of possible conversions are:
/// - [`Relative`], [`Absolute`], or [`Canonical`] into [`Any`].
/// - [`Canonical`] into [`Absolute`].
/// - Any form into itself.
pub trait PathCast<Form: PathForm>: PathForm {}
impl<Form: PathForm> PathCast<Form> for Form {}
impl PathCast<Any> for Relative {}
impl PathCast<Any> for Absolute {}
impl PathCast<Any> for Canonical {}
impl PathCast<Absolute> for Canonical {}
/// A trait used to specify the output [`PathForm`] of a path join operation.
///
/// The output path forms based on the left hand side path form are as follows:
///
/// | Left hand side | Output form |
/// | --------------:|:------------ |
/// | [`Any`] | [`Any`] |
/// | [`Relative`] | [`Any`] |
/// | [`Absolute`] | [`Absolute`] |
/// | [`Canonical`] | [`Absolute`] |
pub trait PathJoin: PathForm {
type Output: PathForm;
}
impl PathJoin for Any {
type Output = Self;
}
impl PathJoin for Relative {
type Output = Any;
}
impl PathJoin for Absolute {
type Output = Self;
}
impl PathJoin for Canonical {
type Output = Absolute;
}
/// A marker trait for [`PathForm`]s that support setting the file name or extension.
///
/// This includes the [`Any`], [`Relative`], and [`Absolute`] path forms.
/// [`Canonical`] paths do not support this, since appending file names and extensions that contain
/// path separators can cause the path to no longer be canonical.
pub trait PathSet: PathForm {}
impl PathSet for Any {}
impl PathSet for Relative {}
impl PathSet for Absolute {}
/// A marker trait for [`PathForm`]s that support pushing [`MaybeRelative`] paths.
///
/// This includes only [`Any`] and [`Absolute`] path forms.
/// Pushing onto a [`Relative`] path could cause it to become [`Absolute`],
/// which is why they do not support pushing.
/// In the future, a `push_rel` and/or a `try_push` method could be added as an alternative.
/// Similarly, [`Canonical`] paths may become uncanonical if a non-canonical path is pushed onto it.
pub trait PathPush: PathSet {}
impl PathPush for Any {}
impl PathPush for Absolute {}

View file

@ -2,12 +2,15 @@ mod assert_path_eq;
mod components; mod components;
pub mod dots; pub mod dots;
pub mod expansions; pub mod expansions;
pub mod form;
mod helpers; mod helpers;
mod path;
mod tilde; mod tilde;
mod trailing_slash; mod trailing_slash;
pub use components::components; pub use components::components;
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs}; pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs};
pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir}; pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir};
pub use path::*;
pub use tilde::expand_tilde; pub use tilde::expand_tilde;
pub use trailing_slash::{has_trailing_slash, strip_trailing_slash}; pub use trailing_slash::{has_trailing_slash, strip_trailing_slash};

3095
crates/nu-path/src/path.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ extend-ignore-re = [
"--find ba\\b", "--find ba\\b",
"0x\\[ba be\\]", "0x\\[ba be\\]",
"\\)BaR'", "\\)BaR'",
"fo<66>.txt",
] ]
[type.rust.extend-words] [type.rust.extend-words]