use camel_component_api::CamelError;
use tokio::fs::OpenOptions;
pub(crate) fn path_contains_traversal(path: &str) -> bool {
std::path::Path::new(path)
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
}
pub(crate) fn validate_relative_filename(raw: &str, label: &str) -> Result<(), CamelError> {
let p = std::path::Path::new(raw);
if raw.is_empty() {
return Err(CamelError::ProcessorError(format!(
"{label} resolved to an empty path"
)));
}
if raw.contains('\0') {
return Err(CamelError::ProcessorError(format!(
"{label} contains a NUL byte"
)));
}
if p.is_absolute() {
return Err(CamelError::ProcessorError(format!(
"{label} must be relative to the endpoint directory, got absolute path: '{raw}'"
)));
}
if path_contains_traversal(raw) {
return Err(CamelError::ProcessorError(format!(
"{label} contains directory traversal: '{raw}'"
)));
}
Ok(())
}
pub(crate) fn open_options_no_follow() -> OpenOptions {
let mut opts = OpenOptions::new();
#[cfg(unix)]
opts.custom_flags(libc::O_NOFOLLOW);
opts
}
pub(crate) fn is_valid_temp_prefix(prefix: &str) -> bool {
!prefix.contains('\0')
&& !std::path::Path::new(prefix).is_absolute()
&& !prefix.contains(std::path::MAIN_SEPARATOR)
&& !prefix.contains('/')
&& !prefix.contains('\\')
}
pub(crate) fn validate_path_is_within_base(
base_dir: &std::path::Path,
target_path: &std::path::Path,
) -> Result<(), CamelError> {
if base_dir.exists() {
let canonical_base = base_dir.canonicalize().map_err(|e| {
CamelError::ProcessorError(format!("Cannot canonicalize base directory: {}", e))
})?;
let rel = target_path.strip_prefix(base_dir).map_err(|_| {
CamelError::ProcessorError(format!(
"Path '{}' is not under base '{}'",
target_path.display(),
base_dir.display()
))
})?;
if path_contains_traversal(&rel.to_string_lossy()) {
return Err(CamelError::ProcessorError(format!(
"Path '{}' contains directory traversal",
target_path.display()
)));
}
let mut cumulative = base_dir.to_path_buf();
for component in rel.components() {
cumulative.push(component);
if std::fs::symlink_metadata(&cumulative).is_ok_and(|m| m.file_type().is_symlink()) {
return Err(CamelError::ProcessorError(format!(
"Path '{}' traverses symlinked component '{}' below base '{}'",
target_path.display(),
cumulative.display(),
base_dir.display()
)));
}
}
let mut ancestor = target_path;
while std::fs::symlink_metadata(ancestor).is_err() {
match ancestor.parent() {
Some(parent) => ancestor = parent,
None => {
ancestor = base_dir;
break;
}
}
}
let canonical_ancestor = ancestor.canonicalize().map_err(|e| {
CamelError::ProcessorError(format!(
"Cannot canonicalize existing ancestor '{}': {}",
ancestor.display(),
e
))
})?;
if !canonical_ancestor.starts_with(&canonical_base) {
return Err(CamelError::ProcessorError(format!(
"Path '{}' is outside base directory '{}'",
canonical_ancestor.display(),
canonical_base.display()
)));
}
} else {
let rel = target_path.strip_prefix(base_dir).map_err(|_| {
CamelError::ProcessorError(format!(
"Path '{}' is not under base '{}'",
target_path.display(),
base_dir.display()
))
})?;
let rel_str = rel.to_string_lossy();
if path_contains_traversal(&rel_str) {
return Err(CamelError::ProcessorError(format!(
"Path '{}' contains directory traversal",
target_path.display()
)));
}
}
Ok(())
}