Skip to main content

lit/
errors.rs

1/// Centralized Error Types for Lit
2/// Provides sanitized error messages that don't leak sensitive information
3/// and machine-readable error codes for agentic consumption
4use std::fmt;
5use std::io;
6
7/// Machine-readable error codes for structured output
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ErrorCode {
10    RepoNotFound,
11    RepoCorrupt,
12    RefNotFound,
13    RefConflict,
14    MergeConflict,
15    IndexLocked,
16    AuthFailed,
17    TransportDenied,
18    CryptoError,
19    ObjectNotFound,
20    InvalidInput,
21    NotImplemented,
22    IoError,
23    ConfigError,
24    GeneralError,
25}
26
27impl ErrorCode {
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            ErrorCode::RepoNotFound => "REPO_NOT_FOUND",
31            ErrorCode::RepoCorrupt => "REPO_CORRUPT",
32            ErrorCode::RefNotFound => "REF_NOT_FOUND",
33            ErrorCode::RefConflict => "REF_CONFLICT",
34            ErrorCode::MergeConflict => "MERGE_CONFLICT",
35            ErrorCode::IndexLocked => "INDEX_LOCKED",
36            ErrorCode::AuthFailed => "AUTH_FAILED",
37            ErrorCode::TransportDenied => "TRANSPORT_DENIED",
38            ErrorCode::CryptoError => "CRYPTO_ERROR",
39            ErrorCode::ObjectNotFound => "OBJECT_NOT_FOUND",
40            ErrorCode::InvalidInput => "INVALID_INPUT",
41            ErrorCode::NotImplemented => "NOT_IMPLEMENTED",
42            ErrorCode::IoError => "IO_ERROR",
43            ErrorCode::ConfigError => "CONFIG_ERROR",
44            ErrorCode::GeneralError => "GENERAL_ERROR",
45        }
46    }
47}
48
49/// Main error type for Lit operations
50#[derive(Debug)]
51pub enum LitError {
52    /// Encryption-related errors (passphrase, key derivation, etc.)
53    Encryption(String),
54    /// I/O errors (file read/write)
55    IO(String),
56    /// Configuration errors
57    Config(String),
58    /// Network-related errors
59    Network(String),
60    /// Repository structure errors
61    Repository(String),
62    /// Git object errors
63    Object(String),
64    /// Index errors
65    Index(String),
66    /// General errors
67    General(String),
68}
69
70impl LitError {
71    /// Create an encryption error with detailed internal message
72    pub fn encryption(internal_msg: impl Into<String>) -> Self {
73        LitError::Encryption(internal_msg.into())
74    }
75
76    /// Create an I/O error with detailed internal message
77    pub fn io(internal_msg: impl Into<String>) -> Self {
78        LitError::IO(internal_msg.into())
79    }
80
81    /// Create a config error with detailed internal message
82    pub fn config(internal_msg: impl Into<String>) -> Self {
83        LitError::Config(internal_msg.into())
84    }
85
86    /// Create a network error with detailed internal message
87    pub fn network(internal_msg: impl Into<String>) -> Self {
88        LitError::Network(internal_msg.into())
89    }
90
91    /// Create a repository error with detailed internal message
92    pub fn repository(internal_msg: impl Into<String>) -> Self {
93        LitError::Repository(internal_msg.into())
94    }
95
96    /// Create an object error with detailed internal message
97    pub fn object(internal_msg: impl Into<String>) -> Self {
98        LitError::Object(internal_msg.into())
99    }
100
101    /// Create an index error with detailed internal message
102    pub fn index(internal_msg: impl Into<String>) -> Self {
103        LitError::Index(internal_msg.into())
104    }
105
106    /// Create a general error with detailed internal message
107    pub fn general(internal_msg: impl Into<String>) -> Self {
108        LitError::General(internal_msg.into())
109    }
110
111    /// Get the internal detailed error message (for logging only)
112    pub fn internal_message(&self) -> &str {
113        match self {
114            LitError::Encryption(msg) => msg,
115            LitError::IO(msg) => msg,
116            LitError::Config(msg) => msg,
117            LitError::Network(msg) => msg,
118            LitError::Repository(msg) => msg,
119            LitError::Object(msg) => msg,
120            LitError::Index(msg) => msg,
121            LitError::General(msg) => msg,
122        }
123    }
124
125    /// Machine-readable error code for structured output
126    pub fn error_code(&self) -> &'static str {
127        match self {
128            LitError::Encryption(_) => ErrorCode::CryptoError.as_str(),
129            LitError::IO(_) => ErrorCode::IoError.as_str(),
130            LitError::Config(_) => ErrorCode::ConfigError.as_str(),
131            LitError::Network(_) => ErrorCode::TransportDenied.as_str(),
132            LitError::Repository(msg) => {
133                if msg.contains("not found")
134                    || msg.contains("No .lit directory")
135                    || msg.contains("find_repo_root")
136                {
137                    ErrorCode::RepoNotFound.as_str()
138                } else {
139                    ErrorCode::RepoCorrupt.as_str()
140                }
141            }
142            LitError::Object(msg) => {
143                if msg.contains("not found") || msg.contains("No such") {
144                    ErrorCode::ObjectNotFound.as_str()
145                } else {
146                    ErrorCode::GeneralError.as_str()
147                }
148            }
149            LitError::Index(_) => ErrorCode::GeneralError.as_str(),
150            LitError::General(msg) => {
151                if msg.contains("not yet implemented") || msg.contains("not yet fully implemented")
152                {
153                    ErrorCode::NotImplemented.as_str()
154                } else if msg.contains("not found") {
155                    ErrorCode::RefNotFound.as_str()
156                } else {
157                    ErrorCode::GeneralError.as_str()
158                }
159            }
160        }
161    }
162
163    /// User-facing error message (safe to display — strips internal details)
164    /// SECURITY: Returns category-based messages to prevent information disclosure (FINDING-003)
165    pub fn user_message(&self) -> &str {
166        match self {
167            LitError::Encryption(_) => "Encryption operation failed",
168            LitError::IO(_) => "I/O operation failed",
169            LitError::Config(_) => "Configuration error",
170            LitError::Network(_) => "Network operation failed",
171            LitError::Repository(msg) => {
172                if msg.contains("not found") || msg.contains("No .lit directory") {
173                    "Not in a Lit repository"
174                } else {
175                    "Repository error"
176                }
177            }
178            LitError::Object(msg) => {
179                if msg.contains("not found") || msg.contains("No such") {
180                    "Object not found"
181                } else {
182                    "Object error"
183                }
184            }
185            LitError::Index(_) => "Index error",
186            LitError::General(msg) => {
187                if msg.contains("not yet implemented") || msg.contains("not yet fully implemented")
188                {
189                    "Feature not yet implemented"
190                } else if msg.contains("not found") {
191                    "Resource not found"
192                } else {
193                    "Operation failed"
194                }
195            }
196        }
197    }
198
199    /// Actionable suggestions for agents to resolve the error
200    pub fn suggestions(&self) -> Vec<&'static str> {
201        match self {
202            LitError::Repository(msg)
203                if msg.contains("not found") || msg.contains("No .lit directory") =>
204            {
205                vec![
206                    "Run 'lit init' to create a repository",
207                    "Check that you are in the correct directory",
208                ]
209            }
210            LitError::Object(_) => {
211                vec![
212                    "Verify the object hash is correct",
213                    "Run 'lit verify' to check repository integrity",
214                ]
215            }
216            LitError::Network(_) => {
217                vec![
218                    "Check remote URL configuration with 'lit remote list'",
219                    "Verify network/airgap settings with 'lit config show'",
220                ]
221            }
222            LitError::Encryption(_) => {
223                vec![
224                    "Verify passphrase is correct",
225                    "Check encryption configuration",
226                ]
227            }
228            LitError::General(msg) if msg.contains("not yet implemented") => {
229                vec!["This feature is planned for a future release"]
230            }
231            _ => vec![],
232        }
233    }
234
235    /// Log detailed error to secure log file (not stdout/stderr)
236    pub fn log_detailed(&self) {
237        // Only log if debug logging is enabled
238        if std::env::var("LIT_DEBUG").is_ok() {
239            let log_path = get_secure_log_path();
240            if let Ok(path) = log_path {
241                use std::fs::OpenOptions;
242                use std::io::Write;
243
244                let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
245                let log_entry =
246                    format!("[{}] {:?}: {}\n", timestamp, self, self.internal_message());
247
248                if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
249                    let _ = file.write_all(log_entry.as_bytes());
250                }
251            }
252        }
253    }
254}
255
256/// Display implementation shows sanitized error messages
257/// SECURITY: Does not expose file paths, internal state, or detailed errors
258impl fmt::Display for LitError {
259    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
260        match self {
261            LitError::Encryption(_) => write!(f, "Encryption operation failed"),
262            LitError::IO(_) => write!(f, "I/O operation failed"),
263            LitError::Config(_) => write!(f, "Configuration error"),
264            LitError::Network(_) => write!(f, "Network operation failed"),
265            LitError::Repository(_) => write!(f, "Repository operation failed"),
266            LitError::Object(_) => write!(f, "Object operation failed"),
267            LitError::Index(_) => write!(f, "Index operation failed"),
268            LitError::General(_) => write!(f, "Operation failed"),
269        }
270    }
271}
272
273impl std::error::Error for LitError {}
274
275/// Convert from io::Error
276impl From<io::Error> for LitError {
277    fn from(err: io::Error) -> Self {
278        LitError::IO(err.to_string())
279    }
280}
281
282/// Convert from String for backward compatibility
283impl From<String> for LitError {
284    fn from(msg: String) -> Self {
285        LitError::General(msg)
286    }
287}
288
289/// Convert from &str for convenience
290impl From<&str> for LitError {
291    fn from(msg: &str) -> Self {
292        LitError::General(msg.to_string())
293    }
294}
295
296/// Get secure log file path
297fn get_secure_log_path() -> Result<std::path::PathBuf, String> {
298    let home = dirs::home_dir().ok_or("Could not determine home directory")?;
299    let log_dir = home.join(".lit").join("logs");
300
301    // Create log directory if it doesn't exist
302    std::fs::create_dir_all(&log_dir)
303        .map_err(|e| format!("Failed to create log directory: {}", e))?;
304
305    let log_file = log_dir.join("debug.log");
306
307    // Ensure restrictive permissions on log file
308    #[cfg(unix)]
309    {
310        use std::fs::OpenOptions;
311        use std::os::unix::fs::PermissionsExt;
312
313        if !log_file.exists() {
314            OpenOptions::new()
315                .create(true)
316                .write(true)
317                .open(&log_file)
318                .ok();
319        }
320
321        if let Ok(metadata) = std::fs::metadata(&log_file) {
322            let mut perms = metadata.permissions();
323            perms.set_mode(0o600); // Owner read/write only
324            std::fs::set_permissions(&log_file, perms).ok();
325        }
326    }
327
328    Ok(log_file)
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_error_display_sanitization() {
337        let err = LitError::encryption("Detailed: failed to decrypt /home/user/secret/file.txt");
338        assert_eq!(err.to_string(), "Encryption operation failed");
339        assert!(!err.to_string().contains("/home"));
340        assert!(!err.to_string().contains("secret"));
341    }
342
343    #[test]
344    fn test_internal_message_access() {
345        let err = LitError::io("Failed to read /etc/shadow");
346        assert_eq!(err.internal_message(), "Failed to read /etc/shadow");
347    }
348
349    #[test]
350    fn test_error_conversion() {
351        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
352        let lit_err: LitError = io_err.into();
353        assert_eq!(lit_err.to_string(), "I/O operation failed");
354    }
355}