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}