zenops-safe-relative-path-validator 0.4.5

Shared traversal-validation logic for `zenops-safe-relative-path`.
Documentation
//! Shared traversal-validation logic for the `zenops-safe-relative-path`
//! family.
//!
//! Both the runtime [`SafeRelativePath`] type and the compile-time
//! [`srpath!`] proc macro need to agree on what counts as a "safe"
//! relative path. The rule lives here so neither side can drift from the
//! other. Application code should depend on [`zenops-safe-relative-path`]
//! instead of pulling this crate in directly — this exists as the seam
//! between the runtime crate and the proc-macro crate.
//!
//! [`SafeRelativePath`]: https://docs.rs/zenops-safe-relative-path/latest/zenops_safe_relative_path/struct.SafeRelativePath.html
//! [`srpath!`]: https://docs.rs/zenops-safe-relative-path/latest/zenops_safe_relative_path/macro.srpath.html
//! [`zenops-safe-relative-path`]: https://docs.rs/zenops-safe-relative-path

use relative_path::{Component, RelativePath};

/// Returns `true` if `path` contains no `..` components.
///
/// The single source of truth for what counts as a safe relative path in
/// this family of crates. A path is safe when every component is either
/// `.` or a normal name segment — anything that would walk out via `..`
/// is rejected, including segments that would notionally cancel
/// (`a/../b` is unsafe even though it normalises to `b`).
///
/// # Why no `..` at all?
///
/// The check is purely lexical so the same rule can run inside the
/// `srpath!` proc macro at compile time, where no filesystem is available.
/// Once you're committed to a lexical check, "normalise first, then
/// reject `..`" becomes unsound: `foo/../bar` normalises to `bar`, but at
/// run time `foo` might be a symlink, and walking through `..` then
/// resolves against the symlink target's parent rather than the original
/// base. Rejecting every `..` outright sidesteps that footgun.
///
/// # Examples
///
/// ```
/// use zenops_safe_relative_path_validator::is_safe_relative_path;
///
/// assert!(is_safe_relative_path("config/app.toml"));
/// assert!(is_safe_relative_path("."));
/// assert!(is_safe_relative_path(""));
///
/// assert!(!is_safe_relative_path("../etc"));
/// assert!(!is_safe_relative_path("a/../b"));
/// ```
pub fn is_safe_relative_path(path: impl AsRef<RelativePath>) -> bool {
    path.as_ref()
        .components()
        .all(|c| matches!(c, Component::CurDir | Component::Normal(_)))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_safe_paths() {
        assert!(is_safe_relative_path("foo/bar"));
        assert!(is_safe_relative_path("./foo/bar"));
        assert!(is_safe_relative_path(""));
        assert!(is_safe_relative_path("."));
    }

    #[test]
    fn test_validate_unsafe_paths() {
        assert!(!is_safe_relative_path("../foo"));
        assert!(!is_safe_relative_path("foo/../bar"));
        assert!(!is_safe_relative_path("foo/../../bar"));
        assert!(!is_safe_relative_path("foo/../../foo/bar"));
        assert!(!is_safe_relative_path(".."));
        assert!(!is_safe_relative_path("a/b/c/../.."));
        assert!(!is_safe_relative_path("a/../.."));
    }
}