1use std::path::PathBuf;
4
5use crate::error::ExtractionError;
6use crate::formats::detect::ArchiveType;
7
8#[derive(Debug, Clone)]
36pub struct VerificationReport {
37 pub status: VerificationStatus,
39
40 pub integrity_status: CheckStatus,
42
43 pub security_status: CheckStatus,
45
46 pub issues: Vec<VerificationIssue>,
48
49 pub total_entries: usize,
51
52 pub suspicious_entries: usize,
54
55 pub total_size: u64,
57
58 pub format: ArchiveType,
60}
61
62impl VerificationReport {
63 #[must_use]
66 pub fn is_safe(&self) -> bool {
67 self.status == VerificationStatus::Pass
68 }
69
70 #[must_use]
72 pub fn has_critical_issues(&self) -> bool {
73 self.issues
74 .iter()
75 .any(|i| i.severity == IssueSeverity::Critical)
76 }
77
78 #[must_use]
80 pub fn issues_by_severity(&self, severity: IssueSeverity) -> Vec<&VerificationIssue> {
81 self.issues
82 .iter()
83 .filter(|i| i.severity == severity)
84 .collect()
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum VerificationStatus {
91 Pass,
93
94 Fail,
96
97 Warning,
99}
100
101impl std::fmt::Display for VerificationStatus {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::Pass => write!(f, "PASS"),
105 Self::Fail => write!(f, "FAIL"),
106 Self::Warning => write!(f, "WARNING"),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum CheckStatus {
114 Pass,
116
117 Fail,
119
120 Warning,
122
123 Skipped,
125}
126
127impl std::fmt::Display for CheckStatus {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 Self::Pass => write!(f, "OK"),
131 Self::Fail => write!(f, "FAILED"),
132 Self::Warning => write!(f, "WARNING"),
133 Self::Skipped => write!(f, "SKIPPED"),
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct VerificationIssue {
141 pub severity: IssueSeverity,
143
144 pub category: IssueCategory,
146
147 pub entry_path: Option<PathBuf>,
149
150 pub message: String,
152
153 pub context: Option<String>,
155}
156
157impl VerificationIssue {
158 #[must_use]
160 pub fn from_error(error: &ExtractionError, entry_path: Option<PathBuf>) -> Self {
161 let (severity, category, message) = match error {
162 ExtractionError::PathTraversal { path } => (
163 IssueSeverity::Critical,
164 IssueCategory::PathTraversal,
165 format!("Path traversal detected: {}", path.display()),
166 ),
167 ExtractionError::SymlinkEscape { path } => (
168 IssueSeverity::Critical,
169 IssueCategory::SymlinkEscape,
170 format!("Symlink escape: {}", path.display()),
171 ),
172 ExtractionError::HardlinkEscape { path } => (
173 IssueSeverity::Critical,
174 IssueCategory::HardlinkEscape,
175 format!("Hardlink escape: {}", path.display()),
176 ),
177 ExtractionError::ZipBomb {
178 compressed,
179 uncompressed,
180 ratio,
181 } => (
182 IssueSeverity::Critical,
183 IssueCategory::ZipBomb,
184 format!(
185 "Potential zip bomb: {ratio:.1}x compression ratio (compressed={compressed}, uncompressed={uncompressed})"
186 ),
187 ),
188 ExtractionError::QuotaExceeded { resource } => (
189 IssueSeverity::High,
190 IssueCategory::QuotaExceeded,
191 format!("{resource}"),
192 ),
193 ExtractionError::InvalidPermissions { path, mode } => (
194 IssueSeverity::Medium,
195 IssueCategory::InvalidPermissions,
196 format!(
197 "Invalid permissions: {} (mode: {:#o})",
198 path.display(),
199 mode
200 ),
201 ),
202 ExtractionError::Io(io_err) => (
203 IssueSeverity::High,
204 IssueCategory::InvalidArchive,
205 format!("I/O error: {io_err}"),
206 ),
207 ExtractionError::UnsupportedFormat => (
208 IssueSeverity::High,
209 IssueCategory::InvalidArchive,
210 "Unsupported archive format".to_string(),
211 ),
212 ExtractionError::InvalidArchive(msg) => (
213 IssueSeverity::High,
214 IssueCategory::InvalidArchive,
215 format!("Invalid archive: {msg}"),
216 ),
217 ExtractionError::SecurityViolation { reason } => (
218 IssueSeverity::High,
219 IssueCategory::SuspiciousPath,
220 format!("Security violation: {reason}"),
221 ),
222 ExtractionError::SourceNotFound { path } => (
223 IssueSeverity::High,
224 IssueCategory::InvalidArchive,
225 format!("Source not found: {}", path.display()),
226 ),
227 ExtractionError::SourceNotAccessible { path } => (
228 IssueSeverity::High,
229 IssueCategory::InvalidArchive,
230 format!("Source not accessible: {}", path.display()),
231 ),
232 ExtractionError::OutputExists { path } => (
233 IssueSeverity::Medium,
234 IssueCategory::InvalidArchive,
235 format!("Output already exists: {}", path.display()),
236 ),
237 ExtractionError::InvalidCompressionLevel { level } => (
238 IssueSeverity::Medium,
239 IssueCategory::InvalidArchive,
240 format!("Invalid compression level: {level}"),
241 ),
242 ExtractionError::UnknownFormat { path } => (
243 IssueSeverity::High,
244 IssueCategory::InvalidArchive,
245 format!("Unknown format: {}", path.display()),
246 ),
247 ExtractionError::InvalidConfiguration { reason } => (
248 IssueSeverity::High,
249 IssueCategory::InvalidArchive,
250 format!("Invalid configuration: {reason}"),
251 ),
252 };
253
254 Self {
255 severity,
256 category,
257 entry_path,
258 message,
259 context: None,
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
266pub enum IssueSeverity {
267 Info,
269
270 Low,
272
273 Medium,
275
276 High,
278
279 Critical,
281}
282
283impl std::fmt::Display for IssueSeverity {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 match self {
286 Self::Critical => write!(f, "CRITICAL"),
287 Self::High => write!(f, "HIGH"),
288 Self::Medium => write!(f, "MEDIUM"),
289 Self::Low => write!(f, "LOW"),
290 Self::Info => write!(f, "INFO"),
291 }
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum IssueCategory {
298 PathTraversal,
300
301 SymlinkEscape,
303
304 HardlinkEscape,
306
307 ZipBomb,
309
310 InvalidPermissions,
312
313 QuotaExceeded,
315
316 InvalidArchive,
318
319 SuspiciousPath,
321
322 ExecutableFile,
324}
325
326impl std::fmt::Display for IssueCategory {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 Self::PathTraversal => write!(f, "Path Traversal"),
330 Self::SymlinkEscape => write!(f, "Symlink Escape"),
331 Self::HardlinkEscape => write!(f, "Hardlink Escape"),
332 Self::ZipBomb => write!(f, "Zip Bomb"),
333 Self::InvalidPermissions => write!(f, "Invalid Permissions"),
334 Self::QuotaExceeded => write!(f, "Quota Exceeded"),
335 Self::InvalidArchive => write!(f, "Invalid Archive"),
336 Self::SuspiciousPath => write!(f, "Suspicious Path"),
337 Self::ExecutableFile => write!(f, "Executable File"),
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use std::io;
346
347 #[test]
348 fn test_verification_status_display() {
349 assert_eq!(VerificationStatus::Pass.to_string(), "PASS");
350 assert_eq!(VerificationStatus::Fail.to_string(), "FAIL");
351 assert_eq!(VerificationStatus::Warning.to_string(), "WARNING");
352 }
353
354 #[test]
355 fn test_check_status_display() {
356 assert_eq!(CheckStatus::Pass.to_string(), "OK");
357 assert_eq!(CheckStatus::Fail.to_string(), "FAILED");
358 assert_eq!(CheckStatus::Warning.to_string(), "WARNING");
359 assert_eq!(CheckStatus::Skipped.to_string(), "SKIPPED");
360 }
361
362 #[test]
363 fn test_issue_severity_display() {
364 assert_eq!(IssueSeverity::Critical.to_string(), "CRITICAL");
365 assert_eq!(IssueSeverity::High.to_string(), "HIGH");
366 assert_eq!(IssueSeverity::Medium.to_string(), "MEDIUM");
367 assert_eq!(IssueSeverity::Low.to_string(), "LOW");
368 assert_eq!(IssueSeverity::Info.to_string(), "INFO");
369 }
370
371 #[test]
372 fn test_issue_severity_ordering() {
373 assert!(IssueSeverity::Critical > IssueSeverity::High);
374 assert!(IssueSeverity::High > IssueSeverity::Medium);
375 assert!(IssueSeverity::Medium > IssueSeverity::Low);
376 assert!(IssueSeverity::Low > IssueSeverity::Info);
377 }
378
379 #[test]
380 fn test_issue_category_display() {
381 assert_eq!(IssueCategory::PathTraversal.to_string(), "Path Traversal");
382 assert_eq!(IssueCategory::SymlinkEscape.to_string(), "Symlink Escape");
383 }
384
385 #[test]
386 fn test_verification_issue_from_path_traversal() {
387 let error = ExtractionError::PathTraversal {
388 path: PathBuf::from("../../etc/passwd"),
389 };
390 let issue = VerificationIssue::from_error(&error, None);
391
392 assert_eq!(issue.severity, IssueSeverity::Critical);
393 assert_eq!(issue.category, IssueCategory::PathTraversal);
394 assert!(issue.message.contains("Path traversal"));
395 }
396
397 #[test]
398 fn test_verification_issue_from_symlink_escape() {
399 let error = ExtractionError::SymlinkEscape {
400 path: PathBuf::from("link"),
401 };
402 let issue = VerificationIssue::from_error(&error, Some(PathBuf::from("link")));
403
404 assert_eq!(issue.severity, IssueSeverity::Critical);
405 assert_eq!(issue.category, IssueCategory::SymlinkEscape);
406 assert!(issue.message.contains("Symlink escape"));
407 }
408
409 #[test]
410 fn test_verification_issue_from_zip_bomb() {
411 let error = ExtractionError::ZipBomb {
412 compressed: 1000,
413 uncompressed: 1_000_000,
414 ratio: 1000.0,
415 };
416 let issue = VerificationIssue::from_error(&error, None);
417
418 assert_eq!(issue.severity, IssueSeverity::Critical);
419 assert_eq!(issue.category, IssueCategory::ZipBomb);
420 assert!(issue.message.contains("zip bomb"));
421 }
422
423 #[test]
424 fn test_verification_issue_from_io_error() {
425 let error = ExtractionError::Io(io::Error::new(io::ErrorKind::NotFound, "not found"));
426 let issue = VerificationIssue::from_error(&error, None);
427
428 assert_eq!(issue.severity, IssueSeverity::High);
429 assert_eq!(issue.category, IssueCategory::InvalidArchive);
430 }
431
432 #[test]
433 fn test_verification_report_is_safe() {
434 let report = VerificationReport {
435 status: VerificationStatus::Pass,
436 integrity_status: CheckStatus::Pass,
437 security_status: CheckStatus::Pass,
438 issues: Vec::new(),
439 total_entries: 10,
440 suspicious_entries: 0,
441 total_size: 1024,
442 format: ArchiveType::TarGz,
443 };
444
445 assert!(report.is_safe());
446 }
447
448 #[test]
449 fn test_verification_report_not_safe() {
450 let report = VerificationReport {
451 status: VerificationStatus::Fail,
452 integrity_status: CheckStatus::Pass,
453 security_status: CheckStatus::Fail,
454 issues: vec![VerificationIssue {
455 severity: IssueSeverity::Critical,
456 category: IssueCategory::PathTraversal,
457 entry_path: None,
458 message: "Test issue".to_string(),
459 context: None,
460 }],
461 total_entries: 10,
462 suspicious_entries: 1,
463 total_size: 1024,
464 format: ArchiveType::TarGz,
465 };
466
467 assert!(!report.is_safe());
468 assert!(report.has_critical_issues());
469 }
470
471 #[test]
472 fn test_verification_report_issues_by_severity() {
473 let report = VerificationReport {
474 status: VerificationStatus::Warning,
475 integrity_status: CheckStatus::Pass,
476 security_status: CheckStatus::Warning,
477 issues: vec![
478 VerificationIssue {
479 severity: IssueSeverity::Critical,
480 category: IssueCategory::PathTraversal,
481 entry_path: None,
482 message: "Critical issue".to_string(),
483 context: None,
484 },
485 VerificationIssue {
486 severity: IssueSeverity::Low,
487 category: IssueCategory::ExecutableFile,
488 entry_path: None,
489 message: "Low issue".to_string(),
490 context: None,
491 },
492 ],
493 total_entries: 10,
494 suspicious_entries: 2,
495 total_size: 1024,
496 format: ArchiveType::TarGz,
497 };
498
499 let critical_issues = report.issues_by_severity(IssueSeverity::Critical);
500 assert_eq!(critical_issues.len(), 1);
501
502 let low_issues = report.issues_by_severity(IssueSeverity::Low);
503 assert_eq!(low_issues.len(), 1);
504 }
505}