1use std::fmt;
21use std::path::Path;
22
23pub const DEFAULT_MAX_TOTAL_BYTES: u64 = 100_000_000;
25
26pub const DEFAULT_MAX_FILE_SIZE: u64 = 10_000_000;
28
29pub const DEFAULT_MAX_FILE_COUNT: u64 = 10_000;
31
32pub const DEFAULT_MAX_DIR_COUNT: u64 = 10_000;
34
35pub const DEFAULT_MAX_PATH_DEPTH: usize = 100;
37
38pub const DEFAULT_MAX_FILENAME_LENGTH: usize = 255;
40
41pub const DEFAULT_MAX_PATH_LENGTH: usize = 4096;
43
44#[derive(Debug, Clone)]
77pub struct FsLimits {
78 pub max_total_bytes: u64,
81
82 pub max_file_size: u64,
85
86 pub max_file_count: u64,
89
90 pub max_dir_count: u64,
95
96 pub max_path_depth: usize,
101
102 pub max_filename_length: usize,
107
108 pub max_path_length: usize,
111}
112
113impl Default for FsLimits {
114 fn default() -> Self {
115 Self {
116 max_total_bytes: DEFAULT_MAX_TOTAL_BYTES,
117 max_file_size: DEFAULT_MAX_FILE_SIZE,
118 max_file_count: DEFAULT_MAX_FILE_COUNT,
119 max_dir_count: DEFAULT_MAX_DIR_COUNT,
120 max_path_depth: DEFAULT_MAX_PATH_DEPTH,
121 max_filename_length: DEFAULT_MAX_FILENAME_LENGTH,
122 max_path_length: DEFAULT_MAX_PATH_LENGTH,
123 }
124 }
125}
126
127impl FsLimits {
128 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn unlimited() -> Self {
158 Self {
159 max_total_bytes: u64::MAX,
160 max_file_size: u64::MAX,
161 max_file_count: u64::MAX,
162 max_dir_count: u64::MAX,
163 max_path_depth: usize::MAX,
164 max_filename_length: usize::MAX,
165 max_path_length: usize::MAX,
166 }
167 }
168
169 pub fn max_total_bytes(mut self, bytes: u64) -> Self {
179 self.max_total_bytes = bytes;
180 self
181 }
182
183 pub fn max_file_size(mut self, bytes: u64) -> Self {
193 self.max_file_size = bytes;
194 self
195 }
196
197 pub fn max_file_count(mut self, count: u64) -> Self {
207 self.max_file_count = count;
208 self
209 }
210
211 pub fn max_dir_count(mut self, count: u64) -> Self {
213 self.max_dir_count = count;
214 self
215 }
216
217 pub fn max_path_depth(mut self, depth: usize) -> Self {
219 self.max_path_depth = depth;
220 self
221 }
222
223 pub fn max_filename_length(mut self, len: usize) -> Self {
225 self.max_filename_length = len;
226 self
227 }
228
229 pub fn max_path_length(mut self, len: usize) -> Self {
231 self.max_path_length = len;
232 self
233 }
234
235 pub fn validate_path(&self, path: &Path) -> Result<(), FsLimitExceeded> {
243 let path_str = path.to_string_lossy();
244 let path_len = path_str.len();
245
246 if path_len > self.max_path_length {
248 return Err(FsLimitExceeded::PathTooLong {
249 length: path_len,
250 limit: self.max_path_length,
251 });
252 }
253
254 let mut depth: usize = 0;
255 for component in path.components() {
256 match component {
257 std::path::Component::Normal(name) => {
258 let name_str = name.to_string_lossy();
259
260 if name_str.len() > self.max_filename_length {
262 return Err(FsLimitExceeded::FilenameTooLong {
263 length: name_str.len(),
264 limit: self.max_filename_length,
265 });
266 }
267
268 if let Some(bad_char) = find_unsafe_path_char(&name_str) {
270 return Err(FsLimitExceeded::UnsafePathChar {
271 character: bad_char,
272 component: name_str.to_string(),
273 });
274 }
275
276 depth += 1;
277 }
278 std::path::Component::ParentDir => {
279 depth = depth.saturating_sub(1);
280 }
281 _ => {}
282 }
283 }
284
285 if depth > self.max_path_depth {
287 return Err(FsLimitExceeded::PathTooDeep {
288 depth,
289 limit: self.max_path_depth,
290 });
291 }
292
293 Ok(())
294 }
295
296 pub fn check_total_bytes(&self, current: u64, additional: u64) -> Result<(), FsLimitExceeded> {
300 let new_total = current.saturating_add(additional);
301 if new_total > self.max_total_bytes {
302 return Err(FsLimitExceeded::TotalBytes {
303 current,
304 additional,
305 limit: self.max_total_bytes,
306 });
307 }
308 Ok(())
309 }
310
311 pub fn check_file_size(&self, size: u64) -> Result<(), FsLimitExceeded> {
313 if size > self.max_file_size {
314 return Err(FsLimitExceeded::FileSize {
315 size,
316 limit: self.max_file_size,
317 });
318 }
319 Ok(())
320 }
321
322 pub fn check_file_count(&self, current: u64) -> Result<(), FsLimitExceeded> {
324 if current >= self.max_file_count {
325 return Err(FsLimitExceeded::FileCount {
326 current,
327 limit: self.max_file_count,
328 });
329 }
330 Ok(())
331 }
332
333 pub fn check_dir_count(&self, current: u64) -> Result<(), FsLimitExceeded> {
335 if current >= self.max_dir_count {
336 return Err(FsLimitExceeded::DirCount {
337 current,
338 limit: self.max_dir_count,
339 });
340 }
341 Ok(())
342 }
343}
344
345fn find_unsafe_path_char(name: &str) -> Option<String> {
353 for ch in name.chars() {
354 if ch.is_ascii_control() {
357 return Some(format!("U+{:04X}", ch as u32));
358 }
359 if ('\u{0080}'..='\u{009F}').contains(&ch) {
361 return Some(format!("U+{:04X}", ch as u32));
362 }
363 if ('\u{202A}'..='\u{202E}').contains(&ch) || ('\u{2066}'..='\u{2069}').contains(&ch) {
365 return Some(format!("U+{:04X} (bidi override)", ch as u32));
366 }
367 }
368 None
369}
370
371#[derive(Debug, Clone)]
373pub enum FsLimitExceeded {
374 TotalBytes {
376 current: u64,
377 additional: u64,
378 limit: u64,
379 },
380 FileSize { size: u64, limit: u64 },
382 FileCount { current: u64, limit: u64 },
384 DirCount { current: u64, limit: u64 },
386 PathTooDeep { depth: usize, limit: usize },
388 FilenameTooLong { length: usize, limit: usize },
390 PathTooLong { length: usize, limit: usize },
392 UnsafePathChar {
394 character: String,
395 component: String,
396 },
397}
398
399impl fmt::Display for FsLimitExceeded {
400 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401 match self {
402 FsLimitExceeded::TotalBytes {
403 current,
404 additional,
405 limit,
406 } => {
407 write!(
408 f,
409 "filesystem full: {} + {} bytes exceeds {} byte limit",
410 current, additional, limit
411 )
412 }
413 FsLimitExceeded::FileSize { size, limit } => {
414 write!(
415 f,
416 "file too large: {} bytes exceeds {} byte limit",
417 size, limit
418 )
419 }
420 FsLimitExceeded::FileCount { current, limit } => {
421 write!(
422 f,
423 "too many files: {} files at {} file limit",
424 current, limit
425 )
426 }
427 FsLimitExceeded::DirCount { current, limit } => {
428 write!(
429 f,
430 "too many directories: {} directories at {} directory limit",
431 current, limit
432 )
433 }
434 FsLimitExceeded::PathTooDeep { depth, limit } => {
435 write!(
436 f,
437 "path too deep: {} levels exceeds {} level limit",
438 depth, limit
439 )
440 }
441 FsLimitExceeded::FilenameTooLong { length, limit } => {
442 write!(
443 f,
444 "filename too long: {} bytes exceeds {} byte limit",
445 length, limit
446 )
447 }
448 FsLimitExceeded::PathTooLong { length, limit } => {
449 write!(
450 f,
451 "path too long: {} bytes exceeds {} byte limit",
452 length, limit
453 )
454 }
455 FsLimitExceeded::UnsafePathChar {
456 character,
457 component,
458 } => {
459 write!(
460 f,
461 "unsafe character {} in path component '{}'",
462 character, component
463 )
464 }
465 }
466 }
467}
468
469impl std::error::Error for FsLimitExceeded {}
470
471#[derive(Debug, Clone, Default)]
475pub struct FsUsage {
476 pub total_bytes: u64,
478 pub file_count: u64,
480 pub dir_count: u64,
482}
483
484impl FsUsage {
485 pub fn new(total_bytes: u64, file_count: u64, dir_count: u64) -> Self {
487 Self {
488 total_bytes,
489 file_count,
490 dir_count,
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use std::path::PathBuf;
499
500 #[test]
501 fn test_default_limits() {
502 let limits = FsLimits::default();
503 assert_eq!(limits.max_total_bytes, 100_000_000);
504 assert_eq!(limits.max_file_size, 10_000_000);
505 assert_eq!(limits.max_file_count, 10_000);
506 assert_eq!(limits.max_dir_count, 10_000);
507 assert_eq!(limits.max_path_depth, 100);
508 assert_eq!(limits.max_filename_length, 255);
509 assert_eq!(limits.max_path_length, 4096);
510 }
511
512 #[test]
513 fn test_unlimited() {
514 let limits = FsLimits::unlimited();
515 assert_eq!(limits.max_total_bytes, u64::MAX);
516 assert_eq!(limits.max_file_size, u64::MAX);
517 assert_eq!(limits.max_file_count, u64::MAX);
518 assert_eq!(limits.max_dir_count, u64::MAX);
519 assert_eq!(limits.max_path_depth, usize::MAX);
520 assert_eq!(limits.max_filename_length, usize::MAX);
521 assert_eq!(limits.max_path_length, usize::MAX);
522 }
523
524 #[test]
525 fn test_builder() {
526 let limits = FsLimits::new()
527 .max_total_bytes(50_000_000)
528 .max_file_size(1_000_000)
529 .max_file_count(100);
530
531 assert_eq!(limits.max_total_bytes, 50_000_000);
532 assert_eq!(limits.max_file_size, 1_000_000);
533 assert_eq!(limits.max_file_count, 100);
534 }
535
536 #[test]
537 fn test_check_total_bytes() {
538 let limits = FsLimits::new().max_total_bytes(1000);
539
540 assert!(limits.check_total_bytes(500, 400).is_ok());
541 assert!(limits.check_total_bytes(500, 500).is_ok());
542 assert!(limits.check_total_bytes(500, 501).is_err());
543 assert!(limits.check_total_bytes(1000, 1).is_err());
544 }
545
546 #[test]
547 fn test_check_file_size() {
548 let limits = FsLimits::new().max_file_size(1000);
549
550 assert!(limits.check_file_size(999).is_ok());
551 assert!(limits.check_file_size(1000).is_ok());
552 assert!(limits.check_file_size(1001).is_err());
553 }
554
555 #[test]
556 fn test_check_file_count() {
557 let limits = FsLimits::new().max_file_count(10);
558
559 assert!(limits.check_file_count(9).is_ok());
560 assert!(limits.check_file_count(10).is_err());
561 assert!(limits.check_file_count(11).is_err());
562 }
563
564 #[test]
565 fn test_error_display() {
566 let err = FsLimitExceeded::TotalBytes {
567 current: 90,
568 additional: 20,
569 limit: 100,
570 };
571 assert!(err.to_string().contains("90"));
572 assert!(err.to_string().contains("20"));
573 assert!(err.to_string().contains("100"));
574
575 let err = FsLimitExceeded::FileSize {
576 size: 200,
577 limit: 100,
578 };
579 assert!(err.to_string().contains("200"));
580 assert!(err.to_string().contains("100"));
581
582 let err = FsLimitExceeded::FileCount {
583 current: 10,
584 limit: 10,
585 };
586 assert!(err.to_string().contains("10"));
587 }
588
589 #[test]
591 fn test_validate_path_depth_ok() {
592 let limits = FsLimits::new().max_path_depth(3);
593 assert!(limits.validate_path(Path::new("/a/b/c")).is_ok());
594 }
595
596 #[test]
597 fn test_validate_path_depth_exceeded() {
598 let limits = FsLimits::new().max_path_depth(3);
599 assert!(limits.validate_path(Path::new("/a/b/c/d")).is_err());
600 let err = limits.validate_path(Path::new("/a/b/c/d")).unwrap_err();
601 assert!(err.to_string().contains("path too deep"));
602 }
603
604 #[test]
605 fn test_validate_path_depth_with_parent_refs() {
606 let limits = FsLimits::new().max_path_depth(3);
607 assert!(limits.validate_path(Path::new("/a/b/../c/d")).is_ok());
609 }
610
611 #[test]
613 fn test_validate_filename_length_ok() {
614 let limits = FsLimits::new().max_filename_length(10);
615 assert!(limits.validate_path(Path::new("/tmp/short.txt")).is_ok());
616 }
617
618 #[test]
619 fn test_validate_filename_length_exceeded() {
620 let limits = FsLimits::new().max_filename_length(10);
621 let long_name = "a".repeat(11);
622 let path = PathBuf::from(format!("/tmp/{}", long_name));
623 assert!(limits.validate_path(&path).is_err());
624 let err = limits.validate_path(&path).unwrap_err();
625 assert!(err.to_string().contains("filename too long"));
626 }
627
628 #[test]
630 fn test_validate_path_length_exceeded() {
631 let limits = FsLimits::new().max_path_length(20);
632 let path = PathBuf::from("/this/is/a/very/long/path/that/exceeds");
633 assert!(limits.validate_path(&path).is_err());
634 let err = limits.validate_path(&path).unwrap_err();
635 assert!(err.to_string().contains("path too long"));
636 }
637
638 #[test]
640 fn test_validate_path_control_char_rejected() {
641 let limits = FsLimits::new();
642 let path = PathBuf::from("/tmp/file\x01name");
643 assert!(limits.validate_path(&path).is_err());
644 let err = limits.validate_path(&path).unwrap_err();
645 assert!(err.to_string().contains("unsafe character"));
646 }
647
648 #[test]
649 fn test_validate_path_bidi_override_rejected() {
650 let limits = FsLimits::new();
651 let path = PathBuf::from("/tmp/file\u{202E}name");
652 assert!(limits.validate_path(&path).is_err());
653 let err = limits.validate_path(&path).unwrap_err();
654 assert!(err.to_string().contains("bidi override"));
655 }
656
657 #[test]
658 fn test_validate_path_normal_unicode_ok() {
659 let limits = FsLimits::new();
660 assert!(limits.validate_path(Path::new("/tmp/café")).is_ok());
662 assert!(limits.validate_path(Path::new("/tmp/文件")).is_ok());
663 }
664}