zenops_safe_relative_path_validator/lib.rs
1//! Shared traversal-validation logic for the `zenops-safe-relative-path`
2//! family.
3//!
4//! Both the runtime [`SafeRelativePath`] type and the compile-time
5//! [`srpath!`] proc macro need to agree on what counts as a "safe"
6//! relative path. The rule lives here so neither side can drift from the
7//! other. Application code should depend on [`zenops-safe-relative-path`]
8//! instead of pulling this crate in directly — this exists as the seam
9//! between the runtime crate and the proc-macro crate.
10//!
11//! [`SafeRelativePath`]: https://docs.rs/zenops-safe-relative-path/latest/zenops_safe_relative_path/struct.SafeRelativePath.html
12//! [`srpath!`]: https://docs.rs/zenops-safe-relative-path/latest/zenops_safe_relative_path/macro.srpath.html
13//! [`zenops-safe-relative-path`]: https://docs.rs/zenops-safe-relative-path
14
15use relative_path::{Component, RelativePath};
16
17/// Returns `true` if `path` contains no `..` components.
18///
19/// The single source of truth for what counts as a safe relative path in
20/// this family of crates. A path is safe when every component is either
21/// `.` or a normal name segment — anything that would walk out via `..`
22/// is rejected, including segments that would notionally cancel
23/// (`a/../b` is unsafe even though it normalises to `b`).
24///
25/// # Why no `..` at all?
26///
27/// The check is purely lexical so the same rule can run inside the
28/// `srpath!` proc macro at compile time, where no filesystem is available.
29/// Once you're committed to a lexical check, "normalise first, then
30/// reject `..`" becomes unsound: `foo/../bar` normalises to `bar`, but at
31/// run time `foo` might be a symlink, and walking through `..` then
32/// resolves against the symlink target's parent rather than the original
33/// base. Rejecting every `..` outright sidesteps that footgun.
34///
35/// # Examples
36///
37/// ```
38/// use zenops_safe_relative_path_validator::is_safe_relative_path;
39///
40/// assert!(is_safe_relative_path("config/app.toml"));
41/// assert!(is_safe_relative_path("."));
42/// assert!(is_safe_relative_path(""));
43///
44/// assert!(!is_safe_relative_path("../etc"));
45/// assert!(!is_safe_relative_path("a/../b"));
46/// ```
47pub fn is_safe_relative_path(path: impl AsRef<RelativePath>) -> bool {
48 path.as_ref()
49 .components()
50 .all(|c| matches!(c, Component::CurDir | Component::Normal(_)))
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn test_validate_safe_paths() {
59 assert!(is_safe_relative_path("foo/bar"));
60 assert!(is_safe_relative_path("./foo/bar"));
61 assert!(is_safe_relative_path(""));
62 assert!(is_safe_relative_path("."));
63 }
64
65 #[test]
66 fn test_validate_unsafe_paths() {
67 assert!(!is_safe_relative_path("../foo"));
68 assert!(!is_safe_relative_path("foo/../bar"));
69 assert!(!is_safe_relative_path("foo/../../bar"));
70 assert!(!is_safe_relative_path("foo/../../foo/bar"));
71 assert!(!is_safe_relative_path(".."));
72 assert!(!is_safe_relative_path("a/b/c/../.."));
73 assert!(!is_safe_relative_path("a/../.."));
74 }
75}