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::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 pub fn log_detailed(&self) {
237 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
256impl 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
275impl From<io::Error> for LitError {
277 fn from(err: io::Error) -> Self {
278 LitError::IO(err.to_string())
279 }
280}
281
282impl From<String> for LitError {
284 fn from(msg: String) -> Self {
285 LitError::General(msg)
286 }
287}
288
289impl From<&str> for LitError {
291 fn from(msg: &str) -> Self {
292 LitError::General(msg.to_string())
293 }
294}
295
296fn 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 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 #[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); 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}