Skip to main content

strict_path/error/
mod.rs

1//! Define error types and helpers for boundary creation and strict/virtual path validation.
2//!
3//! Exposes the crate-wide error enum `StrictPathError`, which captures boundary creation
4//! failures, path resolution errors, and boundary escape attempts. These errors are surfaced
5//! by public constructors and join operations throughout the crate.
6use std::error::Error;
7use std::fmt;
8use std::path::{Path, PathBuf};
9
10/// Maximum characters shown per path in error messages. Paths longer than this are
11/// truncated with `...` in the middle to keep diagnostics readable without hiding
12/// the meaningful prefix and suffix (drive letter / filename).
13const MAX_ERROR_PATH_LEN: usize = 256;
14
15// Internal helper: render error-friendly path display (truncate long values).
16pub(crate) fn truncate_path_display(path: &Path, max_len: usize) -> String {
17    let path_str = path.to_string_lossy();
18    let char_count = path_str.chars().count();
19    if char_count <= max_len {
20        return path_str.into_owned();
21    }
22    let keep = max_len.saturating_sub(5) / 2;
23    let start: String = path_str.chars().take(keep).collect();
24    let mut tail_chars: Vec<char> = path_str.chars().rev().take(keep).collect();
25    tail_chars.reverse();
26    let end: String = tail_chars.into_iter().collect();
27    format!("{start}...{end}")
28}
29
30/// Errors produced by boundary creation and strict/virtual path validation.
31///
32/// Returned by operations that construct `PathBoundary`/`VirtualRoot` or compose
33/// `StrictPath`/`VirtualPath` via joins. Each variant carries enough context for
34/// actionable diagnostics while avoiding leaking unbounded path data into messages.
35#[derive(Debug)]
36#[must_use = "this error indicates a path validation failure — handle it to detect path traversal attacks or invalid boundaries"]
37pub enum StrictPathError {
38    /// The boundary directory is invalid (missing, not a directory, or I/O error).
39    InvalidRestriction {
40        restriction: PathBuf,
41        source: std::io::Error,
42    },
43    /// The attempted path resolves outside the `PathBoundary` — a traversal attack was blocked.
44    PathEscapesBoundary {
45        attempted_path: PathBuf,
46        restriction_boundary: PathBuf,
47    },
48    /// Canonicalization or resolution failed for the given path.
49    PathResolutionError {
50        path: PathBuf,
51        source: std::io::Error,
52    },
53}
54
55impl StrictPathError {
56    // Internal helper: construct `InvalidRestriction`.
57    #[inline]
58    pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
59        Self::InvalidRestriction {
60            restriction,
61            source,
62        }
63    }
64    // Internal helper: construct `PathEscapesBoundary`.
65    #[inline]
66    pub(crate) fn path_escapes_boundary(
67        attempted_path: PathBuf,
68        restriction_boundary: PathBuf,
69    ) -> Self {
70        Self::PathEscapesBoundary {
71            attempted_path,
72            restriction_boundary,
73        }
74    }
75    // Internal helper: construct `PathResolutionError`.
76    #[inline]
77    pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
78        Self::PathResolutionError { path, source }
79    }
80}
81
82impl fmt::Display for StrictPathError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            StrictPathError::InvalidRestriction {
86                restriction,
87                source,
88            } => {
89                write!(
90                    f,
91                    "Invalid PathBoundary: '{}' is not a valid boundary directory ({source}). \
92                     Ensure the path points to an existing directory, or use try_new_create() to auto-create it.",
93                    truncate_path_display(restriction, MAX_ERROR_PATH_LEN)
94                )
95            }
96            StrictPathError::PathEscapesBoundary {
97                attempted_path,
98                restriction_boundary,
99            } => {
100                let truncated_attempted = truncate_path_display(attempted_path, MAX_ERROR_PATH_LEN);
101                let truncated_boundary =
102                    truncate_path_display(restriction_boundary, MAX_ERROR_PATH_LEN);
103                write!(
104                    f,
105                    "Path escapes boundary: '{truncated_attempted}' resolves outside restriction boundary \
106                     '{truncated_boundary}' — this path traversal attempt was blocked. \
107                     Validate untrusted input through strict_join()/virtual_join() which prevents escapes."
108                )
109            }
110            StrictPathError::PathResolutionError { path, source } => {
111                write!(
112                    f,
113                    "Cannot resolve path: '{}' ({source}). \
114                     Ensure the target exists and is accessible, or create parent directories first.",
115                    truncate_path_display(path, MAX_ERROR_PATH_LEN)
116                )
117            }
118        }
119    }
120}
121
122impl Error for StrictPathError {
123    fn source(&self) -> Option<&(dyn Error + 'static)> {
124        match self {
125            StrictPathError::InvalidRestriction { source, .. }
126            | StrictPathError::PathResolutionError { source, .. } => Some(source),
127            StrictPathError::PathEscapesBoundary { .. } => None,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests;