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 { code: ExitCode::Success, message: None }
342 }
343
344 pub fn error(code: ExitCode, message: impl Into<String>) -> Self {
346 Self { code, message: Some(message.into()) }
347 }
348
349 pub fn from_code(code: ExitCode) -> Self {
351 Self { code, message: None }
352 }
353
354 pub fn code(&self) -> ExitCode {
356 self.code
357 }
358
359 pub fn message(&self) -> Option<&str> {
361 self.message.as_deref()
362 }
363
364 pub fn is_success(&self) -> bool {
366 self.code.is_success()
367 }
368
369 pub fn exit(self) -> ! {
371 if let Some(ref msg) = self.message {
372 if self.code.is_success() {
373 println!("{}", msg);
374 } else {
375 eprintln!("Error: {}", msg);
376 }
377 }
378 std::process::exit(self.code.code() as i32)
379 }
380}
381
382impl From<ExitCode> for ExitResult {
383 fn from(code: ExitCode) -> Self {
384 Self::from_code(code)
385 }
386}
387
388pub trait ToExitCode {
390 fn to_exit_code(&self) -> ExitCode;
392}
393
394impl ToExitCode for std::io::Error {
395 fn to_exit_code(&self) -> ExitCode {
396 use std::io::ErrorKind;
397 match self.kind() {
398 ErrorKind::NotFound => ExitCode::NotFound,
399 ErrorKind::PermissionDenied => ExitCode::PermissionDenied,
400 ErrorKind::TimedOut => ExitCode::Timeout,
401 ErrorKind::Interrupted => ExitCode::Interrupted,
402 ErrorKind::WriteZero | ErrorKind::StorageFull => ExitCode::DiskFull,
403 _ => ExitCode::GeneralError,
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn test_exit_code_values() {
414 assert_eq!(ExitCode::Success.code(), 0);
415 assert_eq!(ExitCode::GeneralError.code(), 1);
416 assert_eq!(ExitCode::SecretsDetected.code(), 10);
417 assert_eq!(ExitCode::PiiDetected.code(), 11);
418 assert_eq!(ExitCode::ConfigNotFound.code(), 20);
419 assert_eq!(ExitCode::NotFound.code(), 30);
420 assert_eq!(ExitCode::NoFilesMatched.code(), 40);
421 }
422
423 #[test]
424 fn test_exit_code_categories() {
425 assert_eq!(ExitCode::Success.category(), ExitCodeCategory::Success);
426 assert_eq!(ExitCode::GeneralError.category(), ExitCodeCategory::GeneralError);
427 assert_eq!(ExitCode::SecretsDetected.category(), ExitCodeCategory::SecurityIssue);
428 assert_eq!(ExitCode::ConfigNotFound.category(), ExitCodeCategory::ConfigurationError);
429 assert_eq!(ExitCode::NotFound.category(), ExitCodeCategory::IoError);
430 assert_eq!(ExitCode::NoFilesMatched.category(), ExitCodeCategory::ValidationError);
431 }
432
433 #[test]
434 fn test_is_security_issue() {
435 assert!(ExitCode::SecretsDetected.is_security_issue());
436 assert!(ExitCode::PiiDetected.is_security_issue());
437 assert!(ExitCode::LicenseViolation.is_security_issue());
438 assert!(!ExitCode::Success.is_security_issue());
439 assert!(!ExitCode::NotFound.is_security_issue());
440 }
441
442 #[test]
443 fn test_from_code() {
444 assert_eq!(ExitCode::from_code(0), ExitCode::Success);
445 assert_eq!(ExitCode::from_code(10), ExitCode::SecretsDetected);
446 assert_eq!(ExitCode::from_code(255), ExitCode::GeneralError); }
448
449 #[test]
450 fn test_display() {
451 let code = ExitCode::SecretsDetected;
452 let display = format!("{}", code);
453 assert!(display.contains("SECRETS_DETECTED"));
454 assert!(display.contains("10"));
455 }
456
457 #[test]
458 fn test_exit_result() {
459 let success = ExitResult::success();
460 assert!(success.is_success());
461 assert_eq!(success.code(), ExitCode::Success);
462
463 let error = ExitResult::error(ExitCode::SecretsDetected, "Found API keys");
464 assert!(!error.is_success());
465 assert_eq!(error.code(), ExitCode::SecretsDetected);
466 assert_eq!(error.message(), Some("Found API keys"));
467 }
468
469 #[test]
470 fn test_io_error_conversion() {
471 use std::io::{Error, ErrorKind};
472
473 let not_found = Error::new(ErrorKind::NotFound, "file not found");
474 assert_eq!(not_found.to_exit_code(), ExitCode::NotFound);
475
476 let permission = Error::new(ErrorKind::PermissionDenied, "access denied");
477 assert_eq!(permission.to_exit_code(), ExitCode::PermissionDenied);
478 }
479
480 #[test]
481 fn test_conversions() {
482 let code = ExitCode::SecretsDetected;
483
484 let u8_code: u8 = code.into();
485 assert_eq!(u8_code, 10);
486
487 let i32_code: i32 = code.into();
488 assert_eq!(i32_code, 10);
489 }
490}