1use std::path::{Component, Path, PathBuf};
7
8use path_jail::Jail;
9
10use crate::entry::{EntryInfo, EntryKind};
11use crate::error::Error;
12
13#[derive(Debug, Clone, Default)]
15pub struct ExtractionState {
16 pub files_extracted: usize,
18 pub dirs_created: usize,
20 pub bytes_written: u64,
22 pub entries_skipped: usize,
24}
25
26pub trait Policy: Send + Sync {
28 fn check(&self, entry: &EntryInfo, state: &ExtractionState) -> Result<(), Error>;
32}
33
34pub struct PolicyChain {
36 policies: Vec<Box<dyn Policy>>,
37}
38
39impl PolicyChain {
40 pub fn new() -> Self {
42 Self {
43 policies: Vec::new(),
44 }
45 }
46
47 pub fn with<P: Policy + 'static>(mut self, policy: P) -> Self {
49 self.policies.push(Box::new(policy));
50 self
51 }
52
53 pub fn check_all(&self, entry: &EntryInfo, state: &ExtractionState) -> Result<(), Error> {
55 for policy in &self.policies {
56 policy.check(entry, state)?;
57 }
58 Ok(())
59 }
60}
61
62impl Default for PolicyChain {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68pub struct PathPolicy {
74 jail: Jail,
75}
76
77impl PathPolicy {
78 pub fn new(destination: &Path) -> Result<Self, Error> {
80 let jail = Jail::new(destination).map_err(|e| Error::PathEscape {
81 entry: destination.display().to_string(),
82 detail: e.to_string(),
83 })?;
84 Ok(Self { jail })
85 }
86
87 fn validate_filename(name: &str) -> Result<(), &'static str> {
89 if name.is_empty() {
91 return Err("empty filename");
92 }
93
94 if name.chars().any(|c| c.is_control()) {
96 return Err("contains control characters");
97 }
98
99 if name.contains('\\') {
101 return Err("contains backslash");
102 }
103
104 if name.len() > 1024 {
106 return Err("path too long (>1024 bytes)");
107 }
108
109 if name.split('/').any(|component| component.len() > 255) {
110 return Err("path component too long (>255 bytes)");
111 }
112
113 for component in Path::new(name).components() {
115 if let Component::Normal(s) = component {
116 if let Some(s) = s.to_str() {
117 let s_upper = s.to_ascii_uppercase();
118 let file_stem = s_upper.split('.').next().unwrap_or(&s_upper);
119
120 match file_stem {
121 "CON" | "PRN" | "AUX" | "NUL" | "COM1" | "COM2" | "COM3" | "COM4"
122 | "COM5" | "COM6" | "COM7" | "COM8" | "COM9" | "LPT1" | "LPT2" | "LPT3"
123 | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9" => {
124 return Err("Windows reserved name");
125 }
126 _ => {}
127 }
128 }
129 }
130 }
131
132 Ok(())
133 }
134}
135
136impl Policy for PathPolicy {
137 fn check(&self, entry: &EntryInfo, _state: &ExtractionState) -> Result<(), Error> {
138 if let Err(reason) = Self::validate_filename(&entry.name) {
140 return Err(Error::InvalidFilename {
141 entry: entry.name.clone(),
142 reason: reason.to_string(),
143 });
144 }
145
146 self.jail.join(&entry.name).map_err(|e| Error::PathEscape {
148 entry: entry.name.clone(),
149 detail: e.to_string(),
150 })?;
151
152 Ok(())
153 }
154}
155
156pub struct SizePolicy {
162 pub max_single_file: u64,
164 pub max_total: u64,
166}
167
168impl SizePolicy {
169 pub fn new(max_single_file: u64, max_total: u64) -> Self {
171 Self {
172 max_single_file,
173 max_total,
174 }
175 }
176}
177
178impl Policy for SizePolicy {
179 fn check(&self, entry: &EntryInfo, state: &ExtractionState) -> Result<(), Error> {
180 if entry.size > self.max_single_file {
182 return Err(Error::FileTooLarge {
183 entry: entry.name.clone(),
184 limit: self.max_single_file,
185 size: entry.size,
186 });
187 }
188
189 if state.bytes_written + entry.size > self.max_total {
191 return Err(Error::TotalSizeExceeded {
192 limit: self.max_total,
193 would_be: state.bytes_written + entry.size,
194 });
195 }
196
197 Ok(())
198 }
199}
200
201pub struct CountPolicy {
207 pub max_files: usize,
209}
210
211impl CountPolicy {
212 pub fn new(max_files: usize) -> Self {
214 Self { max_files }
215 }
216}
217
218impl Policy for CountPolicy {
219 fn check(&self, _entry: &EntryInfo, state: &ExtractionState) -> Result<(), Error> {
220 if state.files_extracted >= self.max_files {
221 return Err(Error::FileCountExceeded {
222 limit: self.max_files,
223 attempted: state.files_extracted + 1,
224 });
225 }
226 Ok(())
227 }
228}
229
230pub struct DepthPolicy {
236 pub max_depth: usize,
238}
239
240impl DepthPolicy {
241 pub fn new(max_depth: usize) -> Self {
243 Self { max_depth }
244 }
245}
246
247impl Policy for DepthPolicy {
248 fn check(&self, entry: &EntryInfo, _state: &ExtractionState) -> Result<(), Error> {
249 let depth = Path::new(&entry.name).components().count();
250 if depth > self.max_depth {
251 return Err(Error::PathTooDeep {
252 entry: entry.name.clone(),
253 depth,
254 limit: self.max_depth,
255 });
256 }
257 Ok(())
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
267pub enum SymlinkBehavior {
268 #[default]
270 Skip,
271 Error,
273}
274
275pub struct SymlinkPolicy {
277 pub behavior: SymlinkBehavior,
279}
280
281impl SymlinkPolicy {
282 pub fn new(behavior: SymlinkBehavior) -> Self {
284 Self { behavior }
285 }
286}
287
288impl Policy for SymlinkPolicy {
289 fn check(&self, entry: &EntryInfo, _state: &ExtractionState) -> Result<(), Error> {
290 if let EntryKind::Symlink { target } = &entry.kind {
291 match self.behavior {
292 SymlinkBehavior::Skip => {
293 }
296 SymlinkBehavior::Error => {
297 return Err(Error::SymlinkNotAllowed {
298 entry: entry.name.clone(),
299 target: target.clone(),
300 });
301 }
302 }
303 }
304 Ok(())
305 }
306}
307
308#[derive(Debug, Clone)]
314pub struct PolicyConfig {
315 pub destination: PathBuf,
316 pub max_single_file: u64,
317 pub max_total: u64,
318 pub max_files: usize,
319 pub max_depth: usize,
320 pub symlink_behavior: SymlinkBehavior,
321}
322
323impl PolicyConfig {
324 pub fn build(&self) -> Result<PolicyChain, Error> {
326 Ok(PolicyChain::new()
327 .with(PathPolicy::new(&self.destination)?)
328 .with(SizePolicy::new(self.max_single_file, self.max_total))
329 .with(CountPolicy::new(self.max_files))
330 .with(DepthPolicy::new(self.max_depth))
331 .with(SymlinkPolicy::new(self.symlink_behavior)))
332 }
333}