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)
214 if msg.contains("Encryption not initialized") =>
215 {
216 vec![
217 "Set LIT_PASSPHRASE or LIT_PASSPHRASE_FILE to unlock the repository",
218 "Check encryption settings in .lit/encryption.toml",
219 ]
220 }
221 LitError::Repository(msg)
222 if msg.contains("not found") || msg.contains("No .lit directory") =>
223 {
224 vec![
225 "Run 'lit init' to create a repository",
226 "Check that you are in the correct directory",
227 ]
228 }
229 LitError::Object(_) => {
230 vec![
231 "Verify the object hash is correct",
232 "Run 'lit verify' to check repository integrity",
233 ]
234 }
235 LitError::Network(_) => {
236 vec![
237 "Check remote URL configuration with 'lit remote list'",
238 "Verify network/airgap settings with 'lit config show'",
239 ]
240 }
241 LitError::Encryption(msg) if msg.contains("could not prove it is the agent") => {
247 vec![
248 "Stop the stale agent with 'lit agent stop', then start a new one",
249 "Nothing was sent to the process on that port",
250 ]
251 }
252 LitError::Encryption(_) => {
253 vec![
254 "Verify passphrase is correct",
255 "Check encryption configuration",
256 ]
257 }
258 LitError::General(msg) if msg.contains("not yet implemented") => {
259 vec!["This feature is planned for a future release"]
260 }
261 _ => vec![],
262 }
263 }
264
265 pub fn log_detailed(&self) {
267 if std::env::var("LIT_DEBUG").is_ok() {
269 let log_path = get_secure_log_path();
270 if let Ok(path) = log_path {
271 use std::fs::OpenOptions;
272 use std::io::Write;
273
274 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
275 let log_entry =
276 format!("[{}] {:?}: {}\n", timestamp, self, self.internal_message());
277
278 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
279 let _ = file.write_all(log_entry.as_bytes());
280 }
281 }
282 }
283 }
284}
285
286impl fmt::Display for LitError {
289 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
290 match self {
291 LitError::Encryption(_) => write!(f, "Encryption operation failed"),
292 LitError::IO(_) => write!(f, "I/O operation failed"),
293 LitError::Config(_) => write!(f, "Configuration error"),
294 LitError::Network(_) => write!(f, "Network operation failed"),
295 LitError::Repository(_) => write!(f, "Repository operation failed"),
296 LitError::Object(_) => write!(f, "Object operation failed"),
297 LitError::Index(_) => write!(f, "Index operation failed"),
298 LitError::General(_) => write!(f, "Operation failed"),
299 }
300 }
301}
302
303impl std::error::Error for LitError {}
304
305impl From<io::Error> for LitError {
307 fn from(err: io::Error) -> Self {
308 LitError::IO(err.to_string())
309 }
310}
311
312impl From<String> for LitError {
314 fn from(msg: String) -> Self {
315 LitError::General(msg)
316 }
317}
318
319impl From<&str> for LitError {
321 fn from(msg: &str) -> Self {
322 LitError::General(msg.to_string())
323 }
324}
325
326fn get_secure_log_path() -> Result<std::path::PathBuf, String> {
328 let home = dirs::home_dir().ok_or("Could not determine home directory")?;
329 let log_dir = home.join(".lit").join("logs");
330
331 std::fs::create_dir_all(&log_dir)
333 .map_err(|e| format!("Failed to create log directory: {}", e))?;
334
335 let log_file = log_dir.join("debug.log");
336
337 #[cfg(unix)]
339 {
340 use std::fs::OpenOptions;
341 use std::os::unix::fs::PermissionsExt;
342
343 if !log_file.exists() {
348 OpenOptions::new()
349 .create_new(true)
350 .write(true)
351 .open(&log_file)
352 .ok();
353 }
354
355 if let Ok(metadata) = std::fs::metadata(&log_file) {
356 let mut perms = metadata.permissions();
357 perms.set_mode(0o600); std::fs::set_permissions(&log_file, perms).ok();
359 }
360 }
361
362 Ok(log_file)
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_error_display_sanitization() {
371 let err = LitError::encryption("Detailed: failed to decrypt /home/user/secret/file.txt");
372 assert_eq!(err.to_string(), "Encryption operation failed");
373 assert!(!err.to_string().contains("/home"));
374 assert!(!err.to_string().contains("secret"));
375 }
376
377 #[test]
378 fn test_internal_message_access() {
379 let err = LitError::io("Failed to read /etc/shadow");
380 assert_eq!(err.internal_message(), "Failed to read /etc/shadow");
381 }
382
383 #[test]
384 fn test_error_conversion() {
385 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
386 let lit_err: LitError = io_err.into();
387 assert_eq!(lit_err.to_string(), "I/O operation failed");
388 }
389}