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}