zenops-safe-relative-path 0.5.5

Relative path type that statically prevents `..` traversal.
Documentation
use std::{fmt, sync::Arc};

use relative_path::{RelativePath, RelativePathBuf};
use serde::{de, ser};

use crate::{SafeRelativePath, error::Error};

/// An owned relative path that statically cannot escape its parent.
///
/// `SafeRelativePathBuf` is to [`SafeRelativePath`] what [`PathBuf`] is to
/// [`Path`] in the standard library: same guarantee, same string form,
/// owned instead of borrowed. Construct one by parsing a string with
/// [`str::parse`] (or [`from_relative_path`](Self::from_relative_path)),
/// or deserialise one from anywhere serde reaches.
///
/// All methods on [`SafeRelativePath`] are reachable through [`Deref`], so
/// `try_join`, `to_full_path`, `safe_parent`, and friends are all in scope
/// without an explicit reborrow.
///
/// # Example
///
/// ```
/// use zenops_safe_relative_path::SafeRelativePathBuf;
///
/// let p: SafeRelativePathBuf = "configs/app.toml".parse().unwrap();
/// assert_eq!(p.as_str(), "configs/app.toml");
/// assert_eq!(p.safe_parent().unwrap().as_str(), "configs");
/// ```
///
/// [`PathBuf`]: std::path::PathBuf
/// [`Path`]: std::path::Path
/// [`Deref`]: std::ops::Deref
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SafeRelativePathBuf(RelativePathBuf);

impl SafeRelativePathBuf {
    /// Parse and validate an arbitrary path into a [`SafeRelativePathBuf`].
    ///
    /// Same contract as [`SafeRelativePath::from_relative_path`] — rejects
    /// any path containing `..` — but returns an owned buffer. Most callers
    /// can reach for [`str::parse`] instead, which forwards here.
    pub fn from_relative_path<P>(v: &P) -> Result<Self, Error>
    where
        P: AsRef<RelativePath> + ?Sized,
    {
        SafeRelativePath::from_relative_path(v).map(|p| p.to_safe_relative_path_buf())
    }

    fn as_safe_rel_path(&self) -> &SafeRelativePath {
        unsafe { SafeRelativePath::new_unchecked(&self.0) }
    }
}

impl ser::Serialize for SafeRelativePathBuf {
    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 SafeRelativePathBuf {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "SafeRelativePath".into()
    }

    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        <SafeRelativePath as schemars::JsonSchema>::json_schema(generator)
    }
}

impl<'de> de::Deserialize<'de> for SafeRelativePathBuf {
    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        struct Visitor;
        impl<'de> de::Visitor<'de> for Visitor {
            type Value = SafeRelativePathBuf;

            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "path")
            }

            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
                SafeRelativePathBuf::from_relative_path(v).map_err(de::Error::custom)
            }
        }
        d.deserialize_str(Visitor)
    }
}

impl std::str::FromStr for SafeRelativePathBuf {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::from_relative_path(s)
    }
}

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

impl std::ops::Deref for SafeRelativePathBuf {
    type Target = SafeRelativePath;

    fn deref(&self) -> &Self::Target {
        unsafe { SafeRelativePath::new_unchecked(&self.0) }
    }
}

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

impl AsRef<std::ffi::OsStr> for SafeRelativePathBuf {
    fn as_ref(&self) -> &std::ffi::OsStr {
        self.as_safe_rel_path().as_ref()
    }
}

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

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

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

impl SafeRelativePath {
    /// Copy this borrowed path into an owned [`SafeRelativePathBuf`].
    pub fn to_safe_relative_path_buf(&self) -> SafeRelativePathBuf {
        SafeRelativePathBuf(self.0.to_relative_path_buf())
    }

    /// Collapse `.` components and produce a normalised owned path.
    ///
    /// Unlike [`Path::canonicalize`] this is purely lexical — no filesystem
    /// access. A [`SafeRelativePath`] cannot contain `..` segments, so
    /// normalisation only ever drops `.` components.
    ///
    /// # Example
    ///
    /// ```
    /// use zenops_safe_relative_path::SafeRelativePath;
    ///
    /// let p = SafeRelativePath::from_relative_path("a/./b").unwrap();
    /// assert_eq!(p.normalize_safe().as_str(), "a/b");
    /// ```
    ///
    /// [`Path::canonicalize`]: std::path::Path::canonicalize
    pub fn normalize_safe(&self) -> SafeRelativePathBuf {
        SafeRelativePathBuf(self.0.normalize())
    }

    /// Join another already-safe path onto this one.
    ///
    /// The infallible counterpart to [`try_join`](Self::try_join): both
    /// sides are already known to be safe, so the join cannot introduce
    /// traversal and no validation is needed.
    ///
    /// # Example
    ///
    /// ```
    /// use zenops_safe_relative_path::srpath;
    ///
    /// let joined = srpath!("config").safe_join(srpath!("app.toml"));
    /// assert_eq!(joined.as_str(), "config/app.toml");
    /// ```
    pub fn safe_join(&self, path: impl AsRef<SafeRelativePath>) -> SafeRelativePathBuf {
        SafeRelativePathBuf(self.0.join(&path.as_ref().0))
    }
}