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(_) => {
242 vec![
243 "Verify passphrase is correct",
244 "Check encryption configuration",
245 ]
246 }
247 LitError::General(msg) if msg.contains("not yet implemented") => {
248 vec!["This feature is planned for a future release"]
249 }
250 _ => vec![],
251 }
252 }
253
254 pub fn log_detailed(&self) {
256 if std::env::var("LIT_DEBUG").is_ok() {
258 let log_path = get_secure_log_path();
259 if let Ok(path) = log_path {
260 use std::fs::OpenOptions;
261 use std::io::Write;
262
263 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
264 let log_entry =
265 format!("[{}] {:?}: {}\n", timestamp, self, self.internal_message());
266
267 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
268 let _ = file.write_all(log_entry.as_bytes());
269 }
270 }
271 }
272 }
273}
274
275impl fmt::Display for LitError {
278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279 match self {
280 LitError::Encryption(_) => write!(f, "Encryption operation failed"),
281 LitError::IO(_) => write!(f, "I/O operation failed"),
282 LitError::Config(_) => write!(f, "Configuration error"),
283 LitError::Network(_) => write!(f, "Network operation failed"),
284 LitError::Repository(_) => write!(f, "Repository operation failed"),
285 LitError::Object(_) => write!(f, "Object operation failed"),
286 LitError::Index(_) => write!(f, "Index operation failed"),
287 LitError::General(_) => write!(f, "Operation failed"),
288 }
289 }
290}
291
292impl std::error::Error for LitError {}
293
294impl From<io::Error> for LitError {
296 fn from(err: io::Error) -> Self {
297 LitError::IO(err.to_string())
298 }
299}
300
301impl From<String> for LitError {
303 fn from(msg: String) -> Self {
304 LitError::General(msg)
305 }
306}
307
308impl From<&str> for LitError {
310 fn from(msg: &str) -> Self {
311 LitError::General(msg.to_string())
312 }
313}
314
315fn get_secure_log_path() -> Result<std::path::PathBuf, String> {
317 let home = dirs::home_dir().ok_or("Could not determine home directory")?;
318 let log_dir = home.join(".lit").join("logs");
319
320 std::fs::create_dir_all(&log_dir)
322 .map_err(|e| format!("Failed to create log directory: {}", e))?;
323
324 let log_file = log_dir.join("debug.log");
325
326 #[cfg(unix)]
328 {
329 use std::fs::OpenOptions;
330 use std::os::unix::fs::PermissionsExt;
331
332 if !log_file.exists() {
337 OpenOptions::new()
338 .create_new(true)
339 .write(true)
340 .open(&log_file)
341 .ok();
342 }
343
344 if let Ok(metadata) = std::fs::metadata(&log_file) {
345 let mut perms = metadata.permissions();
346 perms.set_mode(0o600); std::fs::set_permissions(&log_file, perms).ok();
348 }
349 }
350
351 Ok(log_file)
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_error_display_sanitization() {
360 let err = LitError::encryption("Detailed: failed to decrypt /home/user/secret/file.txt");
361 assert_eq!(err.to_string(), "Encryption operation failed");
362 assert!(!err.to_string().contains("/home"));
363 assert!(!err.to_string().contains("secret"));
364 }
365
366 #[test]
367 fn test_internal_message_access() {
368 let err = LitError::io("Failed to read /etc/shadow");
369 assert_eq!(err.internal_message(), "Failed to read /etc/shadow");
370 }
371
372 #[test]
373 fn test_error_conversion() {
374 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
375 let lit_err: LitError = io_err.into();
376 assert_eq!(lit_err.to_string(), "I/O operation failed");
377 }
378}