use std::path::{Path, PathBuf};
use memchr::memchr;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BrrrError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("IO error at {path}: {error}")]
IoWithPath {
error: std::io::Error,
path: PathBuf,
},
#[error("Parse error in {file}: {message}")]
Parse { file: String, message: String },
#[error("Language not supported: {0}")]
UnsupportedLanguage(String),
#[error("Function not found: {0}")]
FunctionNotFound(String),
#[error("Class not found: {0}")]
#[allow(dead_code)]
ClassNotFound(String),
#[error("Tree-sitter error: {0}")]
TreeSitter(String),
#[error("gRPC error: {0}")]
Grpc(#[from] tonic::Status),
#[error("Serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("Cache error: {0}")]
Cache(String),
#[error("Path traversal detected: {target} escapes base directory {base}")]
PathTraversal { target: String, base: String },
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Configuration error: {0}")]
Config(String),
}
pub type Result<T> = std::result::Result<T, BrrrError>;
impl BrrrError {
#[inline]
pub fn io_with_path(error: std::io::Error, path: impl AsRef<Path>) -> Self {
BrrrError::IoWithPath {
error,
path: path.as_ref().to_path_buf(),
}
}
}
pub fn validate_path_containment(
base: &Path,
target: &Path,
) -> Result<std::path::PathBuf> {
let canonical_base = base.canonicalize()?;
let canonical_target = target.canonicalize()?;
if !canonical_target.starts_with(&canonical_base) {
return Err(BrrrError::PathTraversal {
target: target.display().to_string(),
base: base.display().to_string(),
});
}
Ok(canonical_target)
}
#[allow(dead_code)]
pub fn validate_path_safe(base: &Path, target: &Path) -> Result<()> {
let target_str = target.to_string_lossy();
if memchr(0, target_str.as_bytes()).is_some() {
return Err(BrrrError::PathTraversal {
target: "<contains null byte>".to_string(),
base: base.display().to_string(),
});
}
if target.is_absolute() {
let canonical_base = base.canonicalize()?;
if target.exists() {
return validate_path_containment(base, target).map(|_| ());
}
if !target.starts_with(&canonical_base) {
return Err(BrrrError::PathTraversal {
target: target.display().to_string(),
base: base.display().to_string(),
});
}
}
let mut depth: i32 = 0;
for component in target.components() {
match component {
std::path::Component::ParentDir => {
depth -= 1;
if depth < 0 {
return Err(BrrrError::PathTraversal {
target: target.display().to_string(),
base: base.display().to_string(),
});
}
}
std::path::Component::Normal(_) => {
depth += 1;
}
_ => {}
}
}
Ok(())
}