Skip to main content

zenops_safe_relative_path/
buf.rs

1use std::{fmt, sync::Arc};
2
3use relative_path::{RelativePath, RelativePathBuf};
4use serde::{de, ser};
5
6use crate::{SafeRelativePath, error::Error};
7
8/// An owned relative path that statically cannot escape its parent.
9///
10/// `SafeRelativePathBuf` is to [`SafeRelativePath`] what [`PathBuf`] is to
11/// [`Path`] in the standard library: same guarantee, same string form,
12/// owned instead of borrowed. Construct one by parsing a string with
13/// [`str::parse`] (or [`from_relative_path`](Self::from_relative_path)),
14/// or deserialise one from anywhere serde reaches.
15///
16/// All methods on [`SafeRelativePath`] are reachable through [`Deref`], so
17/// `try_join`, `to_full_path`, `safe_parent`, and friends are all in scope
18/// without an explicit reborrow.
19///
20/// # Example
21///
22/// ```
23/// use zenops_safe_relative_path::SafeRelativePathBuf;
24///
25/// let p: SafeRelativePathBuf = "configs/app.toml".parse().unwrap();
26/// assert_eq!(p.as_str(), "configs/app.toml");
27/// assert_eq!(p.safe_parent().unwrap().as_str(), "configs");
28/// ```
29///
30/// [`PathBuf`]: std::path::PathBuf
31/// [`Path`]: std::path::Path
32/// [`Deref`]: std::ops::Deref
33#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct SafeRelativePathBuf(RelativePathBuf);
35
36impl SafeRelativePathBuf {
37    /// Parse and validate an arbitrary path into a [`SafeRelativePathBuf`].
38    ///
39    /// Same contract as [`SafeRelativePath::from_relative_path`] — rejects
40    /// any path containing `..` — but returns an owned buffer. Most callers
41    /// can reach for [`str::parse`] instead, which forwards here.
42    pub fn from_relative_path<P>(v: &P) -> Result<Self, Error>
43    where
44        P: AsRef<RelativePath> + ?Sized,
45    {
46        SafeRelativePath::from_relative_path(v).map(|p| p.to_safe_relative_path_buf())
47    }
48
49    fn as_safe_rel_path(&self) -> &SafeRelativePath {
50        unsafe { SafeRelativePath::new_unchecked(&self.0) }
51    }
52}
53
54impl ser::Serialize for SafeRelativePathBuf {
55    fn serialize<S: ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
56        s.serialize_str(self.as_str())
57    }
58}
59
60#[cfg(feature = "schemars")]
61impl schemars::JsonSchema for SafeRelativePathBuf {
62    fn schema_name() -> std::borrow::Cow<'static, str> {
63        "SafeRelativePath".into()
64    }
65
66    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
67        <SafeRelativePath as schemars::JsonSchema>::json_schema(generator)
68    }
69}
70
71impl<'de> de::Deserialize<'de> for SafeRelativePathBuf {
72    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
73        struct Visitor;
74        impl<'de> de::Visitor<'de> for Visitor {
75            type Value = SafeRelativePathBuf;
76
77            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
78                write!(f, "path")
79            }
80
81            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
82                SafeRelativePathBuf::from_relative_path(v).map_err(de::Error::custom)
83            }
84        }
85        d.deserialize_str(Visitor)
86    }
87}
88
89impl std::str::FromStr for SafeRelativePathBuf {
90    type Err = Error;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        Self::from_relative_path(s)
94    }
95}
96
97impl AsRef<SafeRelativePath> for SafeRelativePathBuf {
98    fn as_ref(&self) -> &SafeRelativePath {
99        self.as_safe_rel_path()
100    }
101}
102
103impl std::ops::Deref for SafeRelativePathBuf {
104    type Target = SafeRelativePath;
105
106    fn deref(&self) -> &Self::Target {
107        unsafe { SafeRelativePath::new_unchecked(&self.0) }
108    }
109}
110
111impl AsRef<RelativePath> for SafeRelativePathBuf {
112    fn as_ref(&self) -> &RelativePath {
113        &self.0
114    }
115}
116
117impl AsRef<std::ffi::OsStr> for SafeRelativePathBuf {
118    fn as_ref(&self) -> &std::ffi::OsStr {
119        self.as_safe_rel_path().as_ref()
120    }
121}
122
123impl fmt::Debug for SafeRelativePathBuf {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        fmt::Debug::fmt(&self.0, f)
126    }
127}
128
129impl fmt::Display for SafeRelativePathBuf {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        fmt::Display::fmt(&self.0, f)
132    }
133}
134
135impl From<SafeRelativePathBuf> for Arc<SafeRelativePath> {
136    fn from(value: SafeRelativePathBuf) -> Self {
137        let arc_rel: Arc<RelativePath> = Arc::from(value.0);
138        unsafe { Arc::from_raw(Arc::into_raw(arc_rel) as *const SafeRelativePath) }
139    }
140}
141
142impl SafeRelativePath {
143    /// Copy this borrowed path into an owned [`SafeRelativePathBuf`].
144    pub fn to_safe_relative_path_buf(&self) -> SafeRelativePathBuf {
145        SafeRelativePathBuf(self.0.to_relative_path_buf())
146    }
147
148    /// Collapse `.` components and produce a normalised owned path.
149    ///
150    /// Unlike [`Path::canonicalize`] this is purely lexical — no filesystem
151    /// access. A [`SafeRelativePath`] cannot contain `..` segments, so
152    /// normalisation only ever drops `.` components.
153    ///
154    /// # Example
155    ///
156    /// ```
157    /// use zenops_safe_relative_path::SafeRelativePath;
158    ///
159    /// let p = SafeRelativePath::from_relative_path("a/./b").unwrap();
160    /// assert_eq!(p.normalize_safe().as_str(), "a/b");
161    /// ```
162    ///
163    /// [`Path::canonicalize`]: std::path::Path::canonicalize
164    pub fn normalize_safe(&self) -> SafeRelativePathBuf {
165        SafeRelativePathBuf(self.0.normalize())
166    }
167
168    /// Join another already-safe path onto this one.
169    ///
170    /// The infallible counterpart to [`try_join`](Self::try_join): both
171    /// sides are already known to be safe, so the join cannot introduce
172    /// traversal and no validation is needed.
173    ///
174    /// # Example
175    ///
176    /// ```
177    /// use zenops_safe_relative_path::srpath;
178    ///
179    /// let joined = srpath!("config").safe_join(srpath!("app.toml"));
180    /// assert_eq!(joined.as_str(), "config/app.toml");
181    /// ```
182    pub fn safe_join(&self, path: impl AsRef<SafeRelativePath>) -> SafeRelativePathBuf {
183        SafeRelativePathBuf(self.0.join(&path.as_ref().0))
184    }
185}