exarch_core/
error.rs

1//! Error types for archive extraction operations.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Result type alias using `ExtractionError`.
7pub type Result<T> = std::result::Result<T, ExtractionError>;
8
9/// Errors that can occur during archive extraction.
10#[derive(Error, Debug)]
11pub enum ExtractionError {
12    /// I/O operation failed.
13    #[error("I/O error: {0}")]
14    Io(#[from] std::io::Error),
15
16    /// Archive format is unsupported or unrecognized.
17    #[error("unsupported archive format")]
18    UnsupportedFormat,
19
20    /// Archive is corrupted or invalid.
21    #[error("invalid archive: {0}")]
22    InvalidArchive(String),
23
24    /// Path traversal attempt detected.
25    #[error("path traversal detected: {path}")]
26    PathTraversal {
27        /// The path that attempted traversal.
28        path: PathBuf,
29    },
30
31    /// Symlink points outside extraction directory.
32    #[error("symlink target outside extraction directory: {path}")]
33    SymlinkEscape {
34        /// The symlink path.
35        path: PathBuf,
36    },
37
38    /// Hardlink target not in extraction directory.
39    #[error("hardlink target outside extraction directory: {path}")]
40    HardlinkEscape {
41        /// The hardlink path.
42        path: PathBuf,
43    },
44
45    /// Potential zip bomb detected.
46    #[error(
47        "potential zip bomb: compressed={compressed} bytes, uncompressed={uncompressed} bytes (ratio: {ratio:.2})"
48    )]
49    ZipBomb {
50        /// Compressed size in bytes.
51        compressed: u64,
52        /// Uncompressed size in bytes.
53        uncompressed: u64,
54        /// Compression ratio.
55        ratio: f64,
56    },
57
58    /// File permissions are invalid or unsafe.
59    #[error("invalid permissions for {path}: {mode:#o}")]
60    InvalidPermissions {
61        /// The file path.
62        path: PathBuf,
63        /// The permission mode.
64        mode: u32,
65    },
66
67    /// Extraction quota exceeded.
68    #[error("quota exceeded: {resource}")]
69    QuotaExceeded {
70        /// Description of the exceeded resource.
71        resource: String,
72    },
73
74    /// Operation not permitted by security policy.
75    #[error("operation denied by security policy: {reason}")]
76    SecurityViolation {
77        /// Reason for the violation.
78        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}