Skip to main content

zenops_safe_relative_path/
lib.rs

1//! Relative paths that statically cannot escape their parent directory.
2//!
3//! A [`SafeRelativePath`] is a relative path whose string form is guaranteed
4//! to contain no `..` components. Joining one onto a base directory can
5//! therefore never resolve to a sibling, ancestor, or cousin of that base.
6//! Putting the type in a function signature pushes the validation out to the
7//! boundary — everything downstream of the signature can trust the input
8//! without re-checking it.
9//!
10//! The crate mirrors the [`Path`] / [`PathBuf`] split from the standard
11//! library:
12//!
13//! - [`SafeRelativePath`] — borrowed, unsized; pass as `&SafeRelativePath`.
14//! - [`SafeRelativePathBuf`] — owned, sized; [`Deref`]s to `SafeRelativePath`.
15//!
16//! Two more specialised types build on the same idea:
17//!
18//! - [`SinglePathComponent`] — narrower still, a single segment with no
19//!   separators. Useful for file names and directory entries.
20//! - [`srpath!`] — a macro that validates a string literal at compile time
21//!   and produces a `&'static SafeRelativePath` with no run-time cost.
22//!
23//! # Example
24//!
25//! ```
26//! use zenops_safe_relative_path::SafeRelativePathBuf;
27//!
28//! let ok: SafeRelativePathBuf = "config/app.toml".parse().unwrap();
29//! assert_eq!(ok.as_str(), "config/app.toml");
30//!
31//! let escaping: Result<SafeRelativePathBuf, _> = "../etc/passwd".parse();
32//! assert!(escaping.is_err());
33//! ```
34//!
35//! # Limitations
36//!
37//! Safety here is *purely lexical* — the crate inspects the path string and
38//! nothing else. Symlinks are not followed, so a `SafeRelativePath` joined
39//! onto a directory that contains a symlink can still reach outside the
40//! base. If symlink containment matters, layer a check on top: canonicalise
41//! the joined path and assert it still starts with the base.
42//!
43//! [`Path`]: std::path::Path
44//! [`PathBuf`]: std::path::PathBuf
45//! [`Deref`]: std::ops::Deref
46
47use std::{
48    fmt,
49    path::{Path, PathBuf},
50    sync::Arc,
51};
52
53use relative_path::RelativePath;
54use serde::ser;
55
56use crate::error::Error;
57
58mod buf;
59pub mod error;
60mod single_path_component;
61
62pub use buf::SafeRelativePathBuf;
63pub use single_path_component::SinglePathComponent;
64
65/// Validate a path literal at compile time and produce a
66/// `&'static `[`SafeRelativePath`].
67///
68/// The macro form of [`SafeRelativePath::from_relative_path`]: it runs the
69/// same check, but on a string literal at compile time, so the runtime cost
70/// is zero. Useful for constants — sentinel paths, hard-coded subdirectory
71/// names, anything that's known when the program is compiled.
72///
73/// A literal containing `..` (or that doesn't parse as a relative path)
74/// becomes a compile error instead of a run-time `Result::Err`.
75///
76/// # Example
77///
78/// ```
79/// use zenops_safe_relative_path::{SafeRelativePath, srpath};
80///
81/// const CONFIG: &SafeRelativePath = srpath!("config/app.toml");
82/// assert_eq!(CONFIG.as_str(), "config/app.toml");
83/// ```
84///
85/// Rejected at compile time:
86///
87/// ```compile_fail
88/// use zenops_safe_relative_path::srpath;
89/// let _ = srpath!("../etc/passwd");
90/// ```
91pub use zenops_safe_relative_path_macros::srpath;
92
93/// A borrowed relative path that statically cannot escape its parent.
94///
95/// This is the borrowed, unsized companion to [`SafeRelativePathBuf`]: same
96/// guarantee, same string form, just held as `&SafeRelativePath`. Use this
97/// type in function signatures to make the caller prove the path is safe
98/// before you touch it; reach for [`SafeRelativePathBuf`] when you need
99/// ownership.
100///
101/// To construct one from a literal that's known at compile time, use the
102/// [`srpath!`](crate::srpath) macro — it validates the literal at compile
103/// time and produces a `&'static SafeRelativePath` with no run-time cost.
104///
105/// For the limits of the guarantee (specifically, what happens with
106/// symlinks), see [the crate-level note](crate#limitations).
107#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
108#[repr(transparent)]
109pub struct SafeRelativePath(RelativePath);
110
111impl SafeRelativePath {
112    /// Reinterpret a `&str` as a [`SafeRelativePath`] without checking it.
113    ///
114    /// # Safety
115    ///
116    /// The caller must guarantee that `v` would succeed if passed through
117    /// [`from_relative_path`](Self::from_relative_path) — it has to parse as
118    /// a [`RelativePath`] and contain no `..` components. Violating this
119    /// hands out a `SafeRelativePath` whose safety invariant doesn't hold,
120    /// and any downstream code that trusts the type is misled.
121    pub const unsafe fn new_unchecked_from_str(v: &str) -> &Self {
122        unsafe { &*(v as *const str as *const RelativePath as *const SafeRelativePath) }
123    }
124
125    /// Reinterpret a `&`[`RelativePath`] as a [`SafeRelativePath`] without
126    /// checking it.
127    ///
128    /// # Safety
129    ///
130    /// The caller must guarantee that `v` contains no `..` components — i.e.
131    /// would succeed if passed through
132    /// [`from_relative_path`](Self::from_relative_path).
133    pub const unsafe fn new_unchecked(v: &RelativePath) -> &Self {
134        unsafe { &*(v as *const RelativePath as *const SafeRelativePath) }
135    }
136
137    /// Try to view an arbitrary [`RelativePath`] as a [`SafeRelativePath`].
138    ///
139    /// Returns [`Error::PathGoesOutsideParent`] if the path contains any
140    /// `..` segment — including ones that would
141    /// notionally cancel out: `a/../b` is rejected even though it normalises
142    /// to `b`. Anything else, including the empty path and `.`, succeeds.
143    ///
144    /// # Example
145    ///
146    /// ```
147    /// use zenops_safe_relative_path::SafeRelativePath;
148    ///
149    /// assert!(SafeRelativePath::from_relative_path("config/app.toml").is_ok());
150    /// assert!(SafeRelativePath::from_relative_path("../etc/passwd").is_err());
151    /// assert!(SafeRelativePath::from_relative_path("a/../b").is_err());
152    /// ```
153    pub fn from_relative_path<P>(v: &P) -> Result<&Self, Error>
154    where
155        P: AsRef<RelativePath> + ?Sized,
156    {
157        let v = v.as_ref();
158
159        if !zenops_safe_relative_path_validator::is_safe_relative_path(v) {
160            return Err(Error::PathGoesOutsideParent(v.to_relative_path_buf()));
161        }
162
163        Ok(unsafe { Self::new_unchecked(v) })
164    }
165
166    /// Join another path onto this one, returning an error if the joined
167    /// segment would escape.
168    ///
169    /// This is the safe counterpart to `Path::join` for inputs that come
170    /// from configuration or another untrusted source: the result is still
171    /// a relative path contained by the original base.
172    ///
173    /// # Example
174    ///
175    /// ```
176    /// use zenops_safe_relative_path::srpath;
177    ///
178    /// let base = srpath!("config");
179    /// assert_eq!(
180    ///     base.try_join("app.toml").unwrap().as_str(),
181    ///     "config/app.toml",
182    /// );
183    /// assert!(base.try_join("../../etc/passwd").is_err());
184    /// ```
185    pub fn try_join(&self, path: impl AsRef<RelativePath>) -> Result<SafeRelativePathBuf, Error> {
186        Ok(self.safe_join(Self::from_relative_path(&path)?))
187    }
188
189    /// View the path as a string slice.
190    pub fn as_str(&self) -> &str {
191        self.0.as_str()
192    }
193
194    /// Resolve this relative path against `base` to produce an absolute
195    /// [`PathBuf`].
196    ///
197    /// Use this at the edge of the program, when a [`SafeRelativePath`]
198    /// finally needs to be handed to a filesystem call against a known
199    /// root — typically `$HOME` or `$XDG_CONFIG_HOME`. The result is `base`
200    /// followed by this path's components, with no `..` traversal between
201    /// them.
202    ///
203    /// # Example
204    ///
205    /// ```
206    /// use std::path::Path;
207    /// use zenops_safe_relative_path::srpath;
208    ///
209    /// let abs = srpath!("config/app.toml").to_full_path(Path::new("/home/ada"));
210    /// assert_eq!(abs, Path::new("/home/ada/config/app.toml"));
211    /// ```
212    pub fn to_full_path(&self, base: impl AsRef<Path>) -> PathBuf {
213        self.0.to_logical_path(base)
214    }
215
216    /// Return the parent path, or [`None`] if there is no parent.
217    ///
218    /// The parent of a [`SafeRelativePath`] is itself a [`SafeRelativePath`]
219    /// — dropping a final component can never introduce traversal.
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use zenops_safe_relative_path::srpath;
225    ///
226    /// assert_eq!(srpath!("a/b/c").safe_parent().unwrap().as_str(), "a/b");
227    /// assert!(srpath!("").safe_parent().is_none());
228    /// ```
229    pub fn safe_parent(&self) -> Option<&SafeRelativePath> {
230        self.0
231            .parent()
232            .map(|p| unsafe { SafeRelativePath::new_unchecked(p) })
233    }
234}
235
236impl ser::Serialize for SafeRelativePath {
237    fn serialize<S: ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
238        s.serialize_str(self.as_str())
239    }
240}
241
242#[cfg(feature = "schemars")]
243impl schemars::JsonSchema for SafeRelativePath {
244    fn schema_name() -> std::borrow::Cow<'static, str> {
245        "SafeRelativePath".into()
246    }
247
248    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
249        schemars::json_schema!({
250            "type": "string",
251            "description": "Relative path that is statically prevented from escaping its parent via `..`.",
252        })
253    }
254}
255
256impl AsRef<RelativePath> for SafeRelativePath {
257    fn as_ref(&self) -> &RelativePath {
258        &self.0
259    }
260}
261
262impl AsRef<std::ffi::OsStr> for SafeRelativePath {
263    fn as_ref(&self) -> &std::ffi::OsStr {
264        self.0.as_str().as_ref()
265    }
266}
267
268impl AsRef<SafeRelativePath> for SafeRelativePath {
269    fn as_ref(&self) -> &SafeRelativePath {
270        self
271    }
272}
273
274impl<'a> From<&'a SafeRelativePath> for SafeRelativePathBuf {
275    fn from(value: &'a SafeRelativePath) -> Self {
276        value.to_safe_relative_path_buf()
277    }
278}
279
280impl<'a> From<&'a SafeRelativePath> for Arc<SafeRelativePath> {
281    fn from(value: &'a SafeRelativePath) -> Self {
282        let arc_rel: Arc<RelativePath> = Arc::from(&value.0);
283        unsafe { Arc::from_raw(Arc::into_raw(arc_rel) as *const SafeRelativePath) }
284    }
285}
286
287impl fmt::Debug for SafeRelativePath {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        fmt::Debug::fmt(&self.0, f)
290    }
291}
292
293impl fmt::Display for SafeRelativePath {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        fmt::Display::fmt(&self.0, f)
296    }
297}