use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
const MAX_ERROR_PATH_LEN: usize = 256;
pub(crate) fn truncate_path_display(path: &Path, max_len: usize) -> String {
let path_str = path.to_string_lossy();
let char_count = path_str.chars().count();
if char_count <= max_len {
return path_str.into_owned();
}
let keep = max_len.saturating_sub(5) / 2;
let start: String = path_str.chars().take(keep).collect();
let mut tail_chars: Vec<char> = path_str.chars().rev().take(keep).collect();
tail_chars.reverse();
let end: String = tail_chars.into_iter().collect();
format!("{start}...{end}")
}
#[derive(Debug)]
#[must_use = "this error indicates a path validation failure — handle it to detect path traversal attacks or invalid boundaries"]
pub enum StrictPathError {
InvalidRestriction {
restriction: PathBuf,
source: std::io::Error,
},
PathEscapesBoundary {
attempted_path: PathBuf,
restriction_boundary: PathBuf,
},
PathResolutionError {
path: PathBuf,
source: std::io::Error,
},
}
impl StrictPathError {
#[inline]
pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
Self::InvalidRestriction {
restriction,
source,
}
}
#[inline]
pub(crate) fn path_escapes_boundary(
attempted_path: PathBuf,
restriction_boundary: PathBuf,
) -> Self {
Self::PathEscapesBoundary {
attempted_path,
restriction_boundary,
}
}
#[inline]
pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
Self::PathResolutionError { path, source }
}
}
impl fmt::Display for StrictPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StrictPathError::InvalidRestriction {
restriction,
source,
} => {
write!(
f,
"Invalid PathBoundary: '{}' is not a valid boundary directory ({source}). \
Ensure the path points to an existing directory, or use try_new_create() to auto-create it.",
truncate_path_display(restriction, MAX_ERROR_PATH_LEN)
)
}
StrictPathError::PathEscapesBoundary {
attempted_path,
restriction_boundary,
} => {
let truncated_attempted = truncate_path_display(attempted_path, MAX_ERROR_PATH_LEN);
let truncated_boundary =
truncate_path_display(restriction_boundary, MAX_ERROR_PATH_LEN);
write!(
f,
"Path escapes boundary: '{truncated_attempted}' resolves outside restriction boundary \
'{truncated_boundary}' — this path traversal attempt was blocked. \
Validate untrusted input through strict_join()/virtual_join() which prevents escapes."
)
}
StrictPathError::PathResolutionError { path, source } => {
write!(
f,
"Cannot resolve path: '{}' ({source}). \
Ensure the target exists and is accessible, or create parent directories first.",
truncate_path_display(path, MAX_ERROR_PATH_LEN)
)
}
}
}
}
impl Error for StrictPathError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
StrictPathError::InvalidRestriction { source, .. }
| StrictPathError::PathResolutionError { source, .. } => Some(source),
StrictPathError::PathEscapesBoundary { .. } => None,
}
}
}
#[cfg(test)]
mod tests;