zenops-safe-relative-path 0.5.5

Relative path type that statically prevents `..` traversal.
Documentation
//! Relative paths that statically cannot escape their parent directory.
//!
//! A [`SafeRelativePath`] is a relative path whose string form is guaranteed
//! to contain no `..` components. Joining one onto a base directory can
//! therefore never resolve to a sibling, ancestor, or cousin of that base.
//! Putting the type in a function signature pushes the validation out to the
//! boundary — everything downstream of the signature can trust the input
//! without re-checking it.
//!
//! The crate mirrors the [`Path`] / [`PathBuf`] split from the standard
//! library:
//!
//! - [`SafeRelativePath`] — borrowed, unsized; pass as `&SafeRelativePath`.
//! - [`SafeRelativePathBuf`] — owned, sized; [`Deref`]s to `SafeRelativePath`.
//!
//! Two more specialised types build on the same idea:
//!
//! - [`SinglePathComponent`] — narrower still, a single segment with no
//!   separators. Useful for file names and directory entries.
//! - [`srpath!`] — a macro that validates a string literal at compile time
//!   and produces a `&'static SafeRelativePath` with no run-time cost.
//!
//! # Example
//!
//! ```
//! use zenops_safe_relative_path::SafeRelativePathBuf;
//!
//! let ok: SafeRelativePathBuf = "config/app.toml".parse().unwrap();
//! assert_eq!(ok.as_str(), "config/app.toml");
//!
//! let escaping: Result<SafeRelativePathBuf, _> = "../etc/passwd".parse();
//! assert!(escaping.is_err());
//! ```
//!
//! # Limitations
//!
//! Safety here is *purely lexical* — the crate inspects the path string and
//! nothing else. Symlinks are not followed, so a `SafeRelativePath` joined
//! onto a directory that contains a symlink can still reach outside the
//! base. If symlink containment matters, layer a check on top: canonicalise
//! the joined path and assert it still starts with the base.
//!
//! [`Path`]: std::path::Path
//! [`PathBuf`]: std::path::PathBuf
//! [`Deref`]: std::ops::Deref

use std::{
    fmt,
    path::{Path, PathBuf},
    sync::Arc,
};

use relative_path::RelativePath;
use serde::ser;

use crate::error::Error;

mod buf;
pub mod error;
mod single_path_component;

pub use buf::SafeRelativePathBuf;
pub use single_path_component::SinglePathComponent;

/// Validate a path literal at compile time and produce a
/// `&'static `[`SafeRelativePath`].
///
/// The macro form of [`SafeRelativePath::from_relative_path`]: it runs the
/// same check, but on a string literal at compile time, so the runtime cost
/// is zero. Useful for constants — sentinel paths, hard-coded subdirectory
/// names, anything that's known when the program is compiled.
///
/// A literal containing `..` (or that doesn't parse as a relative path)
/// becomes a compile error instead of a run-time `Result::Err`.
///
/// # Example
///
/// ```
/// use zenops_safe_relative_path::{SafeRelativePath, srpath};
///
/// const CONFIG: &SafeRelativePath = srpath!("config/app.toml");
/// assert_eq!(CONFIG.as_str(), "config/app.toml");
/// ```
///
/// Rejected at compile time:
///
/// ```compile_fail
/// use zenops_safe_relative_path::srpath;
/// let _ = srpath!("../etc/passwd");
/// ```
pub use zenops_safe_relative_path_macros::srpath;

/// A borrowed relative path that statically cannot escape its parent.
///
/// This is the borrowed, unsized companion to [`SafeRelativePathBuf`]: same
/// guarantee, same string form, just held as `&SafeRelativePath`. Use this
/// type in function signatures to make the caller prove the path is safe
/// before you touch it; reach for [`SafeRelativePathBuf`] when you need
/// ownership.
///
/// To construct one from a literal that's known at compile time, use the
/// [`srpath!`](crate::srpath) macro — it validates the literal at compile
/// time and produces a `&'static SafeRelativePath` with no run-time cost.
///
/// For the limits of the guarantee (specifically, what happens with
/// symlinks), see [the crate-level note](crate#limitations).
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SafeRelativePath(RelativePath);

impl SafeRelativePath {
    /// Reinterpret a `&str` as a [`SafeRelativePath`] without checking it.
    ///
    /// # Safety
    ///
    /// The caller must guarantee that `v` would succeed if passed through
    /// [`from_relative_path`](Self::from_relative_path) — it has to parse as
    /// a [`RelativePath`] and contain no `..` components. Violating this
    /// hands out a `SafeRelativePath` whose safety invariant doesn't hold,
    /// and any downstream code that trusts the type is misled.
    pub const unsafe fn new_unchecked_from_str(v: &str) -> &Self {
        unsafe { &*(v as *const str as *const RelativePath as *const SafeRelativePath) }
    }

    /// Reinterpret a `&`[`RelativePath`] as a [`SafeRelativePath`] without
    /// checking it.
    ///
    /// # Safety
    ///
    /// The caller must guarantee that `v` contains no `..` components — i.e.
    /// would succeed if passed through
    /// [`from_relative_path`](Self::from_relative_path).
    pub const unsafe fn new_unchecked(v: &RelativePath) -> &Self {
        unsafe { &*(v as *const RelativePath as *const SafeRelativePath) }
    }

    /// Try to view an arbitrary [`RelativePath`] as a [`SafeRelativePath`].
    ///
    /// Returns [`Error::PathGoesOutsideParent`] if the path contains any
    /// `..` segment — including ones that would
    /// notionally cancel out: `a/../b` is rejected even though it normalises
    /// to `b`. Anything else, including the empty path and `.`, succeeds.
    ///
    /// # Example
    ///
    /// ```
    /// use zenops_safe_relative_path::SafeRelativePath;
    ///
    /// assert!(SafeRelativePath::from_relative_path("config/app.toml").is_ok());
    /// assert!(SafeRelativePath::from_relative_path("../etc/passwd").is_err());
    /// assert!(SafeRelativePath::from_relative_path("a/../b").is_err());
    /// ```
    pub fn from_relative_path<P>(v: &P) -> Result<&Self, Error>
    where
        P: AsRef<RelativePath> + ?Sized,
    {
        let v = v.as_ref();

        if !zenops_safe_relative_path_validator::is_safe_relative_path(v) {
            return Err(Error::PathGoesOutsideParent(v.to_relative_path_buf()));
        }

        Ok(unsafe { Self::new_unchecked(v) })
    }

    /// Join another path onto this one, returning an error if the joined
    /// segment would escape.
    ///
    /// This is the safe counterpart to `Path::join` for inputs that come
    /// from configuration or another untrusted source: the result is still
    /// a relative path contained by the original base.
    ///
    /// # Example
    ///
    /// ```
    /// use zenops_safe_relative_path::srpath;
    ///
    /// let base = srpath!("config");
    /// assert_eq!(
    ///     base.try_join("app.toml").unwrap().as_str(),
    ///     "config/app.toml",
    /// );
    /// assert!(base.try_join("../../etc/passwd").is_err());
    /// ```
    pub fn try_join(&self, path: impl AsRef<RelativePath>) -> Result<SafeRelativePathBuf, Error> {
        Ok(self.safe_join(Self::from_relative_path(&path)?))
    }

    /// View the path as a string slice.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    /// Resolve this relative path against `base` to produce an absolute
    /// [`PathBuf`].
    ///
    /// Use this at the edge of the program, when a [`SafeRelativePath`]
    /// finally needs to be handed to a filesystem call against a known
    /// root — typically `$HOME` or `$XDG_CONFIG_HOME`. The result is `base`
    /// followed by this path's components, with no `..` traversal between
    /// them.
    ///
    /// # Example
    ///
    /// ```
    /// use std::path::Path;
    /// use zenops_safe_relative_path::srpath;
    ///
    /// let abs = srpath!("config/app.toml").to_full_path(Path::new("/home/ada"));
    /// assert_eq!(abs, Path::new("/home/ada/config/app.toml"));
    /// ```
    pub fn to_full_path(&self, base: impl AsRef<Path>) -> PathBuf {
        self.0.to_logical_path(base)
    }

    /// Return the parent path, or [`None`] if there is no parent.
    ///
    /// The parent of a [`SafeRelativePath`] is itself a [`SafeRelativePath`]
    /// — dropping a final component can never introduce traversal.
    ///
    /// # Example
    ///
    /// ```
    /// use zenops_safe_relative_path::srpath;
    ///
    /// assert_eq!(srpath!("a/b/c").safe_parent().unwrap().as_str(), "a/b");
    /// assert!(srpath!("").safe_parent().is_none());
    /// ```
    pub fn safe_parent(&self) -> Option<&SafeRelativePath> {
        self.0
            .parent()
            .map(|p| unsafe { SafeRelativePath::new_unchecked(p) })
    }
}

impl ser::Serialize for SafeRelativePath {
    fn serialize<S: ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(self.as_str())
    }
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for SafeRelativePath {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "SafeRelativePath".into()
    }

    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
        schemars::json_schema!({
            "type": "string",
            "description": "Relative path that is statically prevented from escaping its parent via `..`.",
        })
    }
}

impl AsRef<RelativePath> for SafeRelativePath {
    fn as_ref(&self) -> &RelativePath {
        &self.0
    }
}

impl AsRef<std::ffi::OsStr> for SafeRelativePath {
    fn as_ref(&self) -> &std::ffi::OsStr {
        self.0.as_str().as_ref()
    }
}

impl AsRef<SafeRelativePath> for SafeRelativePath {
    fn as_ref(&self) -> &SafeRelativePath {
        self
    }
}

impl<'a> From<&'a SafeRelativePath> for SafeRelativePathBuf {
    fn from(value: &'a SafeRelativePath) -> Self {
        value.to_safe_relative_path_buf()
    }
}

impl<'a> From<&'a SafeRelativePath> for Arc<SafeRelativePath> {
    fn from(value: &'a SafeRelativePath) -> Self {
        let arc_rel: Arc<RelativePath> = Arc::from(&value.0);
        unsafe { Arc::from_raw(Arc::into_raw(arc_rel) as *const SafeRelativePath) }
    }
}

impl fmt::Debug for SafeRelativePath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, f)
    }
}

impl fmt::Display for SafeRelativePath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}