1use std::fmt;
54use std::process::ExitCode as StdExitCode;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[repr(u8)]
63pub enum ExitCode {
64 Success = 0,
67
68 GeneralError = 1,
71 ArgumentError = 2,
73 InternalError = 3,
75 Interrupted = 4,
77 Timeout = 5,
79
80 SecretsDetected = 10,
83 PiiDetected = 11,
85 LicenseViolation = 12,
87 PathTraversalBlocked = 13,
89 SecurityScanFailed = 14,
91
92 ConfigNotFound = 20,
95 ConfigInvalid = 21,
97 ConfigMissing = 22,
99 ConfigConflict = 23,
101 ConfigVersionError = 24,
103
104 NotFound = 30,
107 PermissionDenied = 31,
109 DiskFull = 32,
111 NetworkError = 33,
113 ResourceLimitExceeded = 34,
115 FileTooLarge = 35,
117 BinaryFileDetected = 36,
119
120 NoFilesMatched = 40,
123 NoChunksGenerated = 41,
125 InvalidPattern = 42,
127 UnsupportedLanguage = 43,
129 ManifestCorrupted = 44,
131 BudgetExceeded = 45,
133}
134
135impl ExitCode {
136 pub fn code(&self) -> u8 {
138 *self as u8
139 }
140
141 pub fn category(&self) -> ExitCodeCategory {
143 match self.code() {
144 0 => ExitCodeCategory::Success,
145 1..=9 => ExitCodeCategory::GeneralError,
146 10..=19 => ExitCodeCategory::SecurityIssue,
147 20..=29 => ExitCodeCategory::ConfigurationError,
148 30..=39 => ExitCodeCategory::IoError,
149 40..=49 => ExitCodeCategory::ValidationError,
150 _ => ExitCodeCategory::GeneralError,
151 }
152 }
153
154 pub fn name(&self) -> &'static str {
156 match self {
157 Self::Success => "SUCCESS",
158 Self::GeneralError => "GENERAL_ERROR",
159 Self::ArgumentError => "ARGUMENT_ERROR",
160 Self::InternalError => "INTERNAL_ERROR",
161 Self::Interrupted => "INTERRUPTED",
162 Self::Timeout => "TIMEOUT",
163 Self::SecretsDetected => "SECRETS_DETECTED",
164 Self::PiiDetected => "PII_DETECTED",
165 Self::LicenseViolation => "LICENSE_VIOLATION",
166 Self::PathTraversalBlocked => "PATH_TRAVERSAL_BLOCKED",
167 Self::SecurityScanFailed => "SECURITY_SCAN_FAILED",
168 Self::ConfigNotFound => "CONFIG_NOT_FOUND",
169 Self::ConfigInvalid => "CONFIG_INVALID",
170 Self::ConfigMissing => "CONFIG_MISSING",
171 Self::ConfigConflict => "CONFIG_CONFLICT",
172 Self::ConfigVersionError => "CONFIG_VERSION_ERROR",
173 Self::NotFound => "NOT_FOUND",
174 Self::PermissionDenied => "PERMISSION_DENIED",
175 Self::DiskFull => "DISK_FULL",
176 Self::NetworkError => "NETWORK_ERROR",
177 Self::ResourceLimitExceeded => "RESOURCE_LIMIT_EXCEEDED",
178 Self::FileTooLarge => "FILE_TOO_LARGE",
179 Self::BinaryFileDetected => "BINARY_FILE_DETECTED",
180 Self::NoFilesMatched => "NO_FILES_MATCHED",
181 Self::NoChunksGenerated => "NO_CHUNKS_GENERATED",
182 Self::InvalidPattern => "INVALID_PATTERN",
183 Self::UnsupportedLanguage => "UNSUPPORTED_LANGUAGE",
184 Self::ManifestCorrupted => "MANIFEST_CORRUPTED",
185 Self::BudgetExceeded => "BUDGET_EXCEEDED",
186 }
187 }
188
189 pub fn description(&self) -> &'static str {
191 match self {
192 Self::Success => "Operation completed successfully",
193 Self::GeneralError => "An unspecified error occurred",
194 Self::ArgumentError => "Invalid command-line arguments",
195 Self::InternalError => "Internal error (please report this bug)",
196 Self::Interrupted => "Operation was interrupted by user",
197 Self::Timeout => "Operation timed out",
198 Self::SecretsDetected => "Secrets or credentials detected in code",
199 Self::PiiDetected => "Personally identifiable information detected",
200 Self::LicenseViolation => "License compliance violation detected",
201 Self::PathTraversalBlocked => "Path traversal attack blocked",
202 Self::SecurityScanFailed => "Security scan failed to complete",
203 Self::ConfigNotFound => "Configuration file not found",
204 Self::ConfigInvalid => "Invalid configuration file format",
205 Self::ConfigMissing => "Required configuration is missing",
206 Self::ConfigConflict => "Conflicting configuration options",
207 Self::ConfigVersionError => "Unsupported configuration version",
208 Self::NotFound => "File or directory not found",
209 Self::PermissionDenied => "Permission denied",
210 Self::DiskFull => "Disk full or quota exceeded",
211 Self::NetworkError => "Network operation failed",
212 Self::ResourceLimitExceeded => "Resource limit exceeded",
213 Self::FileTooLarge => "File exceeds size limit",
214 Self::BinaryFileDetected => "Binary file detected",
215 Self::NoFilesMatched => "No files matched the specified patterns",
216 Self::NoChunksGenerated => "No chunks were generated",
217 Self::InvalidPattern => "Invalid glob or regex pattern",
218 Self::UnsupportedLanguage => "Unsupported language or file type",
219 Self::ManifestCorrupted => "Manifest file is corrupted",
220 Self::BudgetExceeded => "Token budget exceeded",
221 }
222 }
223
224 pub fn is_security_issue(&self) -> bool {
226 matches!(self.category(), ExitCodeCategory::SecurityIssue)
227 }
228
229 pub fn is_success(&self) -> bool {
231 *self == Self::Success
232 }
233
234 pub fn from_code(code: u8) -> Self {
236 match code {
237 0 => Self::Success,
238 1 => Self::GeneralError,
239 2 => Self::ArgumentError,
240 3 => Self::InternalError,
241 4 => Self::Interrupted,
242 5 => Self::Timeout,
243 10 => Self::SecretsDetected,
244 11 => Self::PiiDetected,
245 12 => Self::LicenseViolation,
246 13 => Self::PathTraversalBlocked,
247 14 => Self::SecurityScanFailed,
248 20 => Self::ConfigNotFound,
249 21 => Self::ConfigInvalid,
250 22 => Self::ConfigMissing,
251 23 => Self::ConfigConflict,
252 24 => Self::ConfigVersionError,
253 30 => Self::NotFound,
254 31 => Self::PermissionDenied,
255 32 => Self::DiskFull,
256 33 => Self::NetworkError,
257 34 => Self::ResourceLimitExceeded,
258 35 => Self::FileTooLarge,
259 36 => Self::BinaryFileDetected,
260 40 => Self::NoFilesMatched,
261 41 => Self::NoChunksGenerated,
262 42 => Self::InvalidPattern,
263 43 => Self::UnsupportedLanguage,
264 44 => Self::ManifestCorrupted,
265 45 => Self::BudgetExceeded,
266 _ => Self::GeneralError,
267 }
268 }
269}
270
271impl fmt::Display for ExitCode {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 write!(f, "{} ({}): {}", self.name(), self.code(), self.description())
274 }
275}
276
277impl From<ExitCode> for u8 {
278 fn from(code: ExitCode) -> Self {
279 code.code()
280 }
281}
282
283impl From<ExitCode> for i32 {
284 fn from(code: ExitCode) -> Self {
285 code.code() as i32
286 }
287}
288
289impl From<ExitCode> for StdExitCode {
290 fn from(code: ExitCode) -> Self {
291 StdExitCode::from(code.code())
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum ExitCodeCategory {
298 Success,
300 GeneralError,
302 SecurityIssue,
304 ConfigurationError,
306 IoError,
308 ValidationError,
310}
311
312impl ExitCodeCategory {
313 pub fn name(&self) -> &'static str {
315 match self {
316 Self::Success => "Success",
317 Self::GeneralError => "General Error",
318 Self::SecurityIssue => "Security Issue",
319 Self::ConfigurationError => "Configuration Error",
320 Self::IoError => "I/O Error",
321 Self::ValidationError => "Validation Error",
322 }
323 }
324}
325
326impl fmt::Display for ExitCodeCategory {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 write!(f, "{}", self.name())
329 }
330}
331
332pub struct ExitResult {
334 code: ExitCode,
335 message: Option<String>,
336}
337
338impl ExitResult {
339 pub fn success() -> Self {
341 Self {
342 code: ExitCode::Success,
343 message: None,
344 }
345 }
346
347 pub fn error(code: ExitCode, message: impl Into<String>) -> Self {
349 Self {
350 code,
351 message: Some(message.into()),
352 }
353 }
354
355 pub fn from_code(code: ExitCode) -> Self {
357 Self {
358 code,
359 message: None,
360 }
361 }
362
363 pub fn code(&self) -> ExitCode {
365 self.code
366 }
367
368 pub fn message(&self) -> Option<&str> {
370 self.message.as_deref()
371 }
372
373 pub fn is_success(&self) -> bool {
375 self.code.is_success()
376 }
377
378 pub fn exit(self) -> ! {
380 if let Some(ref msg) = self.message {
381 if self.code.is_success() {
382 println!("{}", msg);
383 } else {
384 eprintln!("Error: {}", msg);
385 }
386 }
387 std::process::exit(self.code.code() as i32)
388 }
389}
390
391impl From<ExitCode> for ExitResult {
392 fn from(code: ExitCode) -> Self {
393 Self::from_code(code)
394 }
395}
396
397pub trait ToExitCode {
399 fn to_exit_code(&self) -> ExitCode;
401}
402
403impl ToExitCode for std::io::Error {
404 fn to_exit_code(&self) -> ExitCode {
405 use std::io::ErrorKind;
406 match self.kind() {
407 ErrorKind::NotFound => ExitCode::NotFound,
408 ErrorKind::PermissionDenied => ExitCode::PermissionDenied,
409 ErrorKind::TimedOut => ExitCode::Timeout,
410 ErrorKind::Interrupted => ExitCode::Interrupted,
411 ErrorKind::WriteZero | ErrorKind::StorageFull => ExitCode::DiskFull,
412 _ => ExitCode::GeneralError,
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_exit_code_values() {
423 assert_eq!(ExitCode::Success.code(), 0);
424 assert_eq!(ExitCode::GeneralError.code(), 1);
425 assert_eq!(ExitCode::SecretsDetected.code(), 10);
426 assert_eq!(ExitCode::PiiDetected.code(), 11);
427 assert_eq!(ExitCode::ConfigNotFound.code(), 20);
428 assert_eq!(ExitCode::NotFound.code(), 30);
429 assert_eq!(ExitCode::NoFilesMatched.code(), 40);
430 }
431
432 #[test]
433 fn test_exit_code_categories() {
434 assert_eq!(ExitCode::Success.category(), ExitCodeCategory::Success);
435 assert_eq!(ExitCode::GeneralError.category(), ExitCodeCategory::GeneralError);
436 assert_eq!(ExitCode::SecretsDetected.category(), ExitCodeCategory::SecurityIssue);
437 assert_eq!(ExitCode::ConfigNotFound.category(), ExitCodeCategory::ConfigurationError);
438 assert_eq!(ExitCode::NotFound.category(), ExitCodeCategory::IoError);
439 assert_eq!(ExitCode::NoFilesMatched.category(), ExitCodeCategory::ValidationError);
440 }
441
442 #[test]
443 fn test_is_security_issue() {
444 assert!(ExitCode::SecretsDetected.is_security_issue());
445 assert!(ExitCode::PiiDetected.is_security_issue());
446 assert!(ExitCode::LicenseViolation.is_security_issue());
447 assert!(!ExitCode::Success.is_security_issue());
448 assert!(!ExitCode::NotFound.is_security_issue());
449 }
450
451 #[test]
452 fn test_from_code() {
453 assert_eq!(ExitCode::from_code(0), ExitCode::Success);
454 assert_eq!(ExitCode::from_code(10), ExitCode::SecretsDetected);
455 assert_eq!(ExitCode::from_code(255), ExitCode::GeneralError); }
457
458 #[test]
459 fn test_display() {
460 let code = ExitCode::SecretsDetected;
461 let display = format!("{}", code);
462 assert!(display.contains("SECRETS_DETECTED"));
463 assert!(display.contains("10"));
464 }
465
466 #[test]
467 fn test_exit_result() {
468 let success = ExitResult::success();
469 assert!(success.is_success());
470 assert_eq!(success.code(), ExitCode::Success);
471
472 let error = ExitResult::error(ExitCode::SecretsDetected, "Found API keys");
473 assert!(!error.is_success());
474 assert_eq!(error.code(), ExitCode::SecretsDetected);
475 assert_eq!(error.message(), Some("Found API keys"));
476 }
477
478 #[test]
479 fn test_io_error_conversion() {
480 use std::io::{Error, ErrorKind};
481
482 let not_found = Error::new(ErrorKind::NotFound, "file not found");
483 assert_eq!(not_found.to_exit_code(), ExitCode::NotFound);
484
485 let permission = Error::new(ErrorKind::PermissionDenied, "access denied");
486 assert_eq!(permission.to_exit_code(), ExitCode::PermissionDenied);
487 }
488
489 #[test]
490 fn test_conversions() {
491 let code = ExitCode::SecretsDetected;
492
493 let u8_code: u8 = code.into();
494 assert_eq!(u8_code, 10);
495
496 let i32_code: i32 = code.into();
497 assert_eq!(i32_code, 10);
498 }
499}