1use std::path::PathBuf;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, ExtractionError>;
8
9#[derive(Error, Debug)]
11pub enum ExtractionError {
12 #[error("I/O error: {0}")]
14 Io(#[from] std::io::Error),
15
16 #[error("unsupported archive format")]
18 UnsupportedFormat,
19
20 #[error("invalid archive: {0}")]
22 InvalidArchive(String),
23
24 #[error("path traversal detected: {path}")]
26 PathTraversal {
27 path: PathBuf,
29 },
30
31 #[error("symlink target outside extraction directory: {path}")]
33 SymlinkEscape {
34 path: PathBuf,
36 },
37
38 #[error("hardlink target outside extraction directory: {path}")]
40 HardlinkEscape {
41 path: PathBuf,
43 },
44
45 #[error(
47 "potential zip bomb: compressed={compressed} bytes, uncompressed={uncompressed} bytes (ratio: {ratio:.2})"
48 )]
49 ZipBomb {
50 compressed: u64,
52 uncompressed: u64,
54 ratio: f64,
56 },
57
58 #[error("invalid permissions for {path}: {mode:#o}")]
60 InvalidPermissions {
61 path: PathBuf,
63 mode: u32,
65 },
66
67 #[error("quota exceeded: {resource}")]
69 QuotaExceeded {
70 resource: String,
72 },
73
74 #[error("operation denied by security policy: {reason}")]
76 SecurityViolation {
77 reason: String,
79 },
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_error_display() {
88 let err = ExtractionError::UnsupportedFormat;
89 assert_eq!(err.to_string(), "unsupported archive format");
90 }
91
92 #[test]
93 fn test_path_traversal_error() {
94 let err = ExtractionError::PathTraversal {
95 path: PathBuf::from("../etc/passwd"),
96 };
97 assert!(err.to_string().contains("path traversal"));
98 assert!(err.to_string().contains("../etc/passwd"));
99 }
100
101 #[test]
102 fn test_zip_bomb_error() {
103 let err = ExtractionError::ZipBomb {
104 compressed: 1000,
105 uncompressed: 1_000_000,
106 ratio: 1000.0,
107 };
108 assert!(err.to_string().contains("zip bomb"));
109 assert!(err.to_string().contains("1000"));
110 }
111
112 #[test]
113 fn test_io_error_conversion() {
114 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
115 let err: ExtractionError = io_err.into();
116 assert!(matches!(err, ExtractionError::Io(_)));
117 }
118}