exarch_core/security/
quota.rs1use crate::ExtractionError;
4use crate::Result;
5use crate::SecurityConfig;
6
7#[derive(Debug, Default)]
9pub struct QuotaTracker {
10 files_extracted: usize,
11 bytes_written: u64,
12}
13
14impl QuotaTracker {
15 #[must_use]
17 pub fn new() -> Self {
18 Self::default()
19 }
20
21 #[inline]
33 pub fn record_file(&mut self, size: u64, config: &SecurityConfig) -> Result<()> {
34 if config.max_file_size == u64::MAX
37 && config.max_file_count == usize::MAX
38 && config.max_total_size == u64::MAX
39 {
40 self.files_extracted =
41 self.files_extracted
42 .checked_add(1)
43 .ok_or(ExtractionError::QuotaExceeded {
44 resource: crate::QuotaResource::IntegerOverflow,
45 })?;
46
47 self.bytes_written =
48 self.bytes_written
49 .checked_add(size)
50 .ok_or(ExtractionError::QuotaExceeded {
51 resource: crate::QuotaResource::IntegerOverflow,
52 })?;
53
54 return Ok(());
55 }
56
57 self.record_file_checked(size, config)
58 }
59
60 #[inline(never)]
65 fn record_file_checked(&mut self, size: u64, config: &SecurityConfig) -> Result<()> {
66 if size > config.max_file_size {
67 return Err(ExtractionError::QuotaExceeded {
68 resource: crate::QuotaResource::FileSize {
69 size,
70 max: config.max_file_size,
71 },
72 });
73 }
74
75 self.files_extracted =
76 self.files_extracted
77 .checked_add(1)
78 .ok_or(ExtractionError::QuotaExceeded {
79 resource: crate::QuotaResource::IntegerOverflow,
80 })?;
81
82 self.bytes_written =
83 self.bytes_written
84 .checked_add(size)
85 .ok_or(ExtractionError::QuotaExceeded {
86 resource: crate::QuotaResource::IntegerOverflow,
87 })?;
88
89 if self.files_extracted > config.max_file_count {
90 return Err(ExtractionError::QuotaExceeded {
91 resource: crate::QuotaResource::FileCount {
92 current: self.files_extracted,
93 max: config.max_file_count,
94 },
95 });
96 }
97
98 if self.bytes_written > config.max_total_size {
99 return Err(ExtractionError::QuotaExceeded {
100 resource: crate::QuotaResource::TotalSize {
101 current: self.bytes_written,
102 max: config.max_total_size,
103 },
104 });
105 }
106
107 Ok(())
108 }
109
110 #[must_use]
112 pub fn files_extracted(&self) -> usize {
113 self.files_extracted
114 }
115
116 #[must_use]
118 pub fn bytes_written(&self) -> u64 {
119 self.bytes_written
120 }
121}
122
123#[cfg(test)]
124#[allow(clippy::field_reassign_with_default)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_quota_tracker_new() {
130 let tracker = QuotaTracker::new();
131 assert_eq!(tracker.files_extracted(), 0);
132 assert_eq!(tracker.bytes_written(), 0);
133 }
134
135 #[test]
136 fn test_quota_tracker_record_file() {
137 let mut tracker = QuotaTracker::new();
138 let config = SecurityConfig::default();
139
140 assert!(tracker.record_file(1000, &config).is_ok());
141 assert_eq!(tracker.files_extracted(), 1);
142 assert_eq!(tracker.bytes_written(), 1000);
143 }
144
145 #[test]
146 fn test_quota_tracker_exceed_file_count() {
147 let mut tracker = QuotaTracker::new();
148 let mut config = SecurityConfig::default();
149 config.max_file_count = 2;
150
151 assert!(tracker.record_file(100, &config).is_ok());
152 assert!(tracker.record_file(100, &config).is_ok());
153 let result = tracker.record_file(100, &config);
154 assert!(matches!(result, Err(ExtractionError::QuotaExceeded { .. })));
155 }
156
157 #[test]
158 fn test_quota_tracker_exceed_total_size() {
159 let mut tracker = QuotaTracker::new();
160 let mut config = SecurityConfig::default();
161 config.max_total_size = 1000;
162
163 assert!(tracker.record_file(600, &config).is_ok());
164 let result = tracker.record_file(500, &config);
165 assert!(matches!(result, Err(ExtractionError::QuotaExceeded { .. })));
166 }
167
168 #[test]
169 fn test_quota_tracker_exceed_file_size() {
170 let mut tracker = QuotaTracker::new();
171 let mut config = SecurityConfig::default();
172 config.max_file_size = 1000;
173
174 let result = tracker.record_file(2000, &config);
175 assert!(matches!(result, Err(ExtractionError::QuotaExceeded { .. })));
176 }
177
178 #[test]
180 fn test_quota_exactly_at_file_count_limit() {
181 let mut tracker = QuotaTracker::new();
182 let mut config = SecurityConfig::default();
183 config.max_file_count = 3;
184 config.max_total_size = u64::MAX;
185 config.max_file_size = u64::MAX;
186
187 assert!(
189 tracker.record_file(100, &config).is_ok(),
190 "file 1 should succeed"
191 );
192 assert!(
193 tracker.record_file(100, &config).is_ok(),
194 "file 2 should succeed"
195 );
196 assert!(
197 tracker.record_file(100, &config).is_ok(),
198 "file 3 should succeed"
199 );
200 assert_eq!(tracker.files_extracted(), 3, "should have 3 files");
201
202 let result = tracker.record_file(100, &config);
204 assert!(
205 matches!(
206 result,
207 Err(ExtractionError::QuotaExceeded {
208 resource: crate::QuotaResource::FileCount { current: 4, max: 3 }
209 })
210 ),
211 "file 4 should exceed quota"
212 );
213 }
214
215 #[test]
216 fn test_quota_exactly_at_total_size_limit() {
217 let mut tracker = QuotaTracker::new();
218 let mut config = SecurityConfig::default();
219 config.max_file_count = 100;
220 config.max_total_size = 1000;
221 config.max_file_size = u64::MAX;
222
223 assert!(tracker.record_file(600, &config).is_ok());
225 assert_eq!(tracker.bytes_written(), 600);
226
227 assert!(tracker.record_file(400, &config).is_ok());
228 assert_eq!(tracker.bytes_written(), 1000, "should be exactly at limit");
229
230 let result = tracker.record_file(1, &config);
232 assert!(
233 matches!(
234 result,
235 Err(ExtractionError::QuotaExceeded {
236 resource: crate::QuotaResource::TotalSize {
237 current: 1001,
238 max: 1000
239 }
240 })
241 ),
242 "exceeding total size should fail"
243 );
244 }
245
246 #[test]
247 fn test_quota_exactly_at_file_size_limit() {
248 let mut tracker = QuotaTracker::new();
249 let mut config = SecurityConfig::default();
250 config.max_file_count = 100;
251 config.max_total_size = u64::MAX;
252 config.max_file_size = 5000;
253
254 assert!(
256 tracker.record_file(5000, &config).is_ok(),
257 "file exactly at limit should succeed"
258 );
259
260 let result = tracker.record_file(5001, &config);
262 assert!(
263 matches!(
264 result,
265 Err(ExtractionError::QuotaExceeded {
266 resource: crate::QuotaResource::FileSize {
267 size: 5001,
268 max: 5000
269 }
270 })
271 ),
272 "file exceeding limit should fail"
273 );
274 }
275
276 #[test]
277 fn test_quota_off_by_one_file_count() {
278 let mut tracker = QuotaTracker::new();
279 let mut config = SecurityConfig::default();
280 config.max_file_count = 1;
281 config.max_total_size = u64::MAX;
282 config.max_file_size = u64::MAX;
283
284 assert!(tracker.record_file(100, &config).is_ok());
286
287 let result = tracker.record_file(100, &config);
289 assert!(matches!(result, Err(ExtractionError::QuotaExceeded { .. })));
290 }
291
292 #[test]
294 fn test_quota_fast_path_unlimited() {
295 let mut tracker = QuotaTracker::new();
296 let mut config = SecurityConfig::default();
297 config.max_file_size = u64::MAX;
299 config.max_file_count = usize::MAX;
300 config.max_total_size = u64::MAX;
301
302 for i in 1..=1000 {
303 assert!(
304 tracker.record_file(1000, &config).is_ok(),
305 "file {i} should succeed with unlimited quotas"
306 );
307 }
308
309 assert_eq!(tracker.files_extracted(), 1000);
310 assert_eq!(tracker.bytes_written(), 1_000_000);
311 }
312
313 #[test]
315 fn test_quota_fast_path_overflow_detection() {
316 let mut tracker = QuotaTracker::new();
317 let mut config = SecurityConfig::default();
318 config.max_file_size = u64::MAX;
319 config.max_file_count = usize::MAX;
320 config.max_total_size = u64::MAX;
321
322 tracker.bytes_written = u64::MAX - 100;
324
325 let result = tracker.record_file(200, &config);
327 assert!(
328 matches!(
329 result,
330 Err(ExtractionError::QuotaExceeded {
331 resource: crate::QuotaResource::IntegerOverflow
332 })
333 ),
334 "fast path should still detect overflow"
335 );
336 }
337}