go_brrr/error.rs
1//! Central error types for go-brrr.
2//!
3//! Uses `thiserror` for ergonomic error definitions with automatic
4//! `Display` and `From` implementations.
5
6use std::path::{Path, PathBuf};
7
8use memchr::memchr;
9use thiserror::Error;
10
11/// Main error type for the library.
12#[derive(Error, Debug)]
13pub enum BrrrError {
14 /// IO operation failed (without path context - prefer IoWithPath when path is available)
15 #[error("IO error: {0}")]
16 Io(#[from] std::io::Error),
17
18 /// IO operation failed with path context for better error messages
19 #[error("IO error at {path}: {error}")]
20 IoWithPath {
21 error: std::io::Error,
22 path: PathBuf,
23 },
24
25 /// Failed to parse source file
26 #[error("Parse error in {file}: {message}")]
27 Parse { file: String, message: String },
28
29 /// Requested language is not supported
30 #[error("Language not supported: {0}")]
31 UnsupportedLanguage(String),
32
33 /// Function not found in file
34 #[error("Function not found: {0}")]
35 FunctionNotFound(String),
36
37 /// Class not found in file
38 #[error("Class not found: {0}")]
39 #[allow(dead_code)]
40 ClassNotFound(String),
41
42 /// Tree-sitter parsing/query error
43 #[error("Tree-sitter error: {0}")]
44 TreeSitter(String),
45
46 /// gRPC communication error
47 #[error("gRPC error: {0}")]
48 Grpc(#[from] tonic::Status),
49
50 /// JSON serialization/deserialization error
51 #[error("Serialization error: {0}")]
52 Serde(#[from] serde_json::Error),
53
54 /// Cache operation error
55 #[error("Cache error: {0}")]
56 Cache(String),
57
58 /// Path traversal attack detected (security)
59 #[error("Path traversal detected: {target} escapes base directory {base}")]
60 PathTraversal { target: String, base: String },
61
62 /// Invalid argument provided to a function
63 #[error("Invalid argument: {0}")]
64 InvalidArgument(String),
65
66 /// Configuration error (e.g., invalid ignore patterns)
67 #[error("Configuration error: {0}")]
68 Config(String),
69}
70
71/// Convenience type alias for Results using BrrrError.
72pub type Result<T> = std::result::Result<T, BrrrError>;
73
74impl BrrrError {
75 /// Create an IO error with path context.
76 ///
77 /// Use this when reading/writing files to provide actionable error messages
78 /// that include the file path that failed.
79 ///
80 /// # Example
81 ///
82 /// ```ignore
83 /// let source = std::fs::read(path)
84 /// .map_err(|e| BrrrError::io_with_path(e, path))?;
85 /// ```
86 #[inline]
87 pub fn io_with_path(error: std::io::Error, path: impl AsRef<Path>) -> Self {
88 BrrrError::IoWithPath {
89 error,
90 path: path.as_ref().to_path_buf(),
91 }
92 }
93}
94
95/// Validates that a target path is safely contained within a base directory.
96///
97/// This function prevents path traversal attacks where an attacker could use:
98/// - Relative paths with `..` components (e.g., `../../../etc/passwd`)
99/// - Symlinks pointing outside the base directory
100/// - Encoded path separators or other bypass techniques
101///
102/// # Arguments
103///
104/// * `base` - The base directory that must contain the target
105/// * `target` - The target path to validate
106///
107/// # Returns
108///
109/// * `Ok(canonical_target)` - The canonicalized target path if safely contained
110/// * `Err(BrrrError::PathTraversal)` - If the target escapes the base directory
111/// * `Err(BrrrError::Io)` - If paths cannot be canonicalized (e.g., don't exist)
112///
113/// # Security
114///
115/// Both paths are canonicalized to resolve:
116/// - Symlinks (followed to their real targets)
117/// - `.` and `..` components
118/// - Redundant separators
119///
120/// The canonical target must start with the canonical base path prefix.
121///
122/// # Example
123///
124/// ```no_run
125/// use std::path::Path;
126/// use go_brrr::error::validate_path_containment;
127///
128/// let base = Path::new("/project");
129/// let safe = Path::new("/project/src/main.rs");
130/// let unsafe_path = Path::new("/project/../etc/passwd");
131///
132/// assert!(validate_path_containment(base, safe).is_ok());
133/// assert!(validate_path_containment(base, unsafe_path).is_err());
134/// ```
135pub fn validate_path_containment(
136 base: &Path,
137 target: &Path,
138) -> Result<std::path::PathBuf> {
139 let canonical_base = base.canonicalize()?;
140 let canonical_target = target.canonicalize()?;
141
142 if !canonical_target.starts_with(&canonical_base) {
143 return Err(BrrrError::PathTraversal {
144 target: target.display().to_string(),
145 base: base.display().to_string(),
146 });
147 }
148
149 Ok(canonical_target)
150}
151
152/// Validates a path doesn't escape its base without requiring the target to exist.
153///
154/// This is useful for validating user-provided paths before attempting to access them.
155/// Unlike `validate_path_containment`, this function works with non-existent paths
156/// by checking for dangerous patterns in the path components.
157///
158/// # Arguments
159///
160/// * `base` - The base directory (must exist for canonicalization)
161/// * `target` - The target path to validate (may not exist)
162///
163/// # Returns
164///
165/// * `Ok(())` - If the path appears safe
166/// * `Err(BrrrError::PathTraversal)` - If dangerous path components are detected
167///
168/// # Security
169///
170/// Checks for:
171/// - `..` components that could escape the base
172/// - Absolute paths that don't start with the base
173/// - Null bytes or other dangerous characters
174#[allow(dead_code)]
175pub fn validate_path_safe(base: &Path, target: &Path) -> Result<()> {
176 // Check for null bytes (could bypass string checks)
177 // Uses SIMD-accelerated memchr for O(n/16) performance on x86_64/aarch64
178 let target_str = target.to_string_lossy();
179 if memchr(0, target_str.as_bytes()).is_some() {
180 return Err(BrrrError::PathTraversal {
181 target: "<contains null byte>".to_string(),
182 base: base.display().to_string(),
183 });
184 }
185
186 // For absolute paths, verify they're under the base
187 if target.is_absolute() {
188 // Canonicalize base for comparison
189 let canonical_base = base.canonicalize()?;
190
191 // If target exists, use canonicalize for accurate check
192 if target.exists() {
193 return validate_path_containment(base, target).map(|_| ());
194 }
195
196 // Target doesn't exist - check path prefix (less secure but best we can do)
197 // Normalize the absolute target path by resolving what we can
198 if !target.starts_with(&canonical_base) {
199 return Err(BrrrError::PathTraversal {
200 target: target.display().to_string(),
201 base: base.display().to_string(),
202 });
203 }
204 }
205
206 // Check for parent directory traversal attempts
207 let mut depth: i32 = 0;
208 for component in target.components() {
209 match component {
210 std::path::Component::ParentDir => {
211 depth -= 1;
212 if depth < 0 {
213 return Err(BrrrError::PathTraversal {
214 target: target.display().to_string(),
215 base: base.display().to_string(),
216 });
217 }
218 }
219 std::path::Component::Normal(_) => {
220 depth += 1;
221 }
222 _ => {}
223 }
224 }
225
226 Ok(())
227}