1use std::fmt;
5use std::io;
6
7#[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#[derive(Debug)]
51pub enum LitError {
52 Encryption(String),
54 IO(String),
56 Config(String),
58 Network(String),
60 Repository(String),
62 Object(String),
64 Index(String),
66 General(String),
68}
69
70impl LitError {
71 pub fn encryption(internal_msg: impl Into<String>) -> Self {
73 LitError::Encryption(internal_msg.into())
74 }
75
76 pub fn io(internal_msg: impl Into<String>) -> Self {
78 LitError::IO(internal_msg.into())
79 }
80
81 pub fn config(internal_msg: impl Into<String>) -> Self {
83 LitError::Config(internal_msg.into())
84 }
85
86 pub fn network(internal_msg: impl Into<String>) -> Self {
88 LitError::Network(internal_msg.into())
89 }
90
91 pub fn repository(internal_msg: impl Into<String>) -> Self {
93 LitError::Repository(internal_msg.into())
94 }
95
96 pub fn object(internal_msg: impl Into<String>) -> Self {
98 LitError::Object(internal_msg.into())
99 }
100
101 pub fn index(internal_msg: impl Into<String>) -> Self {
103 LitError::Index(internal_msg.into())
104 }
105
106 pub fn general(internal_msg: impl Into<String>) -> Self {
108 LitError::General(internal_msg.into())
109 }
110
111 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 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 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 pub fn suggestions(&self) -> Vec<&'static str> {
201 match self {
202 LitError::General(msg) | LitError::IO(msg)
206 if msg.contains("no Lit encryption header") =>
207 {
208 vec![
209 "Encryption cannot be enabled for a repository that already has commits",
210 "Create a new repository with encryption enabled and import into it",
211 ]
212 }
213 LitError::General(msg) | LitError::IO(msg) | LitError::Encryption(msg)
218 if msg.contains("its key file is missing") =>
219 {
220 vec![
221 "The repository is encrypted and its key file is gone; a new key cannot open it",
222 "Restore the key file from backup, or point key_file in .lit/encryption.toml at it",
223 ]
224 }
225 LitError::General(msg) | LitError::IO(msg) | LitError::Encryption(msg)
228 if msg.contains("shared key file") =>
229 {
230 vec![
231 "This key file is shared between repositories and holds a single passphrase",
232 "Remove key_file from .lit/encryption.toml to give this repository its own key",
233 ]
234 }
235 LitError::General(msg) | LitError::IO(msg)
236 if msg.contains("Encryption not initialized") =>
237 {
238 vec![
239 "Set LIT_PASSPHRASE or LIT_PASSPHRASE_FILE to unlock the repository",
240 "Check encryption settings in .lit/encryption.toml",
241 ]
242 }
243 LitError::Repository(msg)
244 if msg.contains("not found") || msg.contains("No .lit directory") =>
245 {
246 vec![
247 "Run 'lit init' to create a repository",
248 "Check that you are in the correct directory",
249 ]
250 }
251 LitError::Object(_) => {
252 vec![
253 "Verify the object hash is correct",
254 "Run 'lit verify' to check repository integrity",
255 ]
256 }
257 LitError::Network(_) => {
258 vec![
259 "Check remote URL configuration with 'lit remote list'",
260 "Verify network/airgap settings with 'lit config show'",
261 ]
262 }
263 LitError::Encryption(msg) if msg.contains("could not prove it is the agent") => {
269 vec![
270 "Stop the stale agent with 'lit agent stop', then start a new one",
271 "Nothing was sent to the process on that port",
272 ]
273 }
274 LitError::Encryption(_) => {
275 vec![
276 "Verify passphrase is correct",
277 "Check encryption configuration",
278 ]
279 }
280 LitError::General(msg) if msg.contains("not yet implemented") => {
281 vec!["This feature is planned for a future release"]
282 }
283 _ => vec![],
284 }
285 }
286
287 pub fn log_detailed(&self) {
289 if std::env::var("LIT_DEBUG").is_ok() {
291 let log_path = get_secure_log_path();
292 if let Ok(path) = log_path {
293 use std::fs::OpenOptions;
294 use std::io::Write;
295
296 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
297 let log_entry =
298 format!("[{}] {:?}: {}\n", timestamp, self, self.internal_message());
299
300 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
301 let _ = file.write_all(log_entry.as_bytes());
302 }
303 }
304 }
305 }
306}
307
308impl fmt::Display for LitError {
311 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
312 match self {
313 LitError::Encryption(_) => write!(f, "Encryption operation failed"),
314 LitError::IO(_) => write!(f, "I/O operation failed"),
315 LitError::Config(_) => write!(f, "Configuration error"),
316 LitError::Network(_) => write!(f, "Network operation failed"),
317 LitError::Repository(_) => write!(f, "Repository operation failed"),
318 LitError::Object(_) => write!(f, "Object operation failed"),
319 LitError::Index(_) => write!(f, "Index operation failed"),
320 LitError::General(_) => write!(f, "Operation failed"),
321 }
322 }
323}
324
325impl std::error::Error for LitError {}
326
327impl From<io::Error> for LitError {
329 fn from(err: io::Error) -> Self {
330 LitError::IO(err.to_string())
331 }
332}
333
334impl From<String> for LitError {
336 fn from(msg: String) -> Self {
337 LitError::General(msg)
338 }
339}
340
341impl From<&str> for LitError {
343 fn from(msg: &str) -> Self {
344 LitError::General(msg.to_string())
345 }
346}
347
348fn get_secure_log_path() -> Result<std::path::PathBuf, String> {
350 let home = dirs::home_dir().ok_or("Could not determine home directory")?;
351 let log_dir = home.join(".lit").join("logs");
352
353 std::fs::create_dir_all(&log_dir)
355 .map_err(|e| format!("Failed to create log directory: {}", e))?;
356
357 let log_file = log_dir.join("debug.log");
358
359 #[cfg(unix)]
361 {
362 use std::fs::OpenOptions;
363 use std::os::unix::fs::PermissionsExt;
364
365 if !log_file.exists() {
370 OpenOptions::new()
371 .create_new(true)
372 .write(true)
373 .open(&log_file)
374 .ok();
375 }
376
377 if let Ok(metadata) = std::fs::metadata(&log_file) {
378 let mut perms = metadata.permissions();
379 perms.set_mode(0o600); std::fs::set_permissions(&log_file, perms).ok();
381 }
382 }
383
384 Ok(log_file)
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_error_display_sanitization() {
393 let err = LitError::encryption("Detailed: failed to decrypt /home/user/secret/file.txt");
394 assert_eq!(err.to_string(), "Encryption operation failed");
395 assert!(!err.to_string().contains("/home"));
396 assert!(!err.to_string().contains("secret"));
397 }
398
399 #[test]
400 fn test_internal_message_access() {
401 let err = LitError::io("Failed to read /etc/shadow");
402 assert_eq!(err.internal_message(), "Failed to read /etc/shadow");
403 }
404
405 #[test]
406 fn test_error_conversion() {
407 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
408 let lit_err: LitError = io_err.into();
409 assert_eq!(lit_err.to_string(), "I/O operation failed");
410 }
411}