1use crate::field::{FieldError, FieldResult, FormField, Widget};
2
3const DEFAULT_FILE_MAX_SIZE: u64 = 10 * 1024 * 1024;
5
6const DEFAULT_IMAGE_MAX_SIZE: u64 = 5 * 1024 * 1024;
8
9fn validate_filename_safety(filename: &str) -> FieldResult<()> {
12 if filename.contains('\0') {
14 return Err(FieldError::Validation(
15 "Filename contains null bytes".to_string(),
16 ));
17 }
18
19 for component in filename.split(['/', '\\']) {
23 if component == ".." {
24 return Err(FieldError::Validation(
25 "Filename contains directory traversal sequence".to_string(),
26 ));
27 }
28 }
29
30 if filename.starts_with('/') {
32 return Err(FieldError::Validation(
33 "Filename must not be an absolute path".to_string(),
34 ));
35 }
36
37 if filename.starts_with("\\\\") {
39 return Err(FieldError::Validation(
40 "Filename must not be an absolute path".to_string(),
41 ));
42 }
43
44 let bytes = filename.as_bytes();
46 if bytes.len() >= 3
47 && bytes[0].is_ascii_alphabetic()
48 && bytes[1] == b':'
49 && (bytes[2] == b'\\' || bytes[2] == b'/')
50 {
51 return Err(FieldError::Validation(
52 "Filename must not be an absolute path".to_string(),
53 ));
54 }
55
56 Ok(())
57}
58
59pub struct FileField {
61 pub name: String,
63 pub label: Option<String>,
65 pub required: bool,
67 pub help_text: Option<String>,
69 pub widget: Widget,
71 pub initial: Option<serde_json::Value>,
73 pub max_length: Option<usize>,
75 pub allow_empty_file: bool,
77 pub max_size: u64,
79}
80
81impl FileField {
82 pub fn new(name: String) -> Self {
94 Self {
95 name,
96 label: None,
97 required: true,
98 help_text: None,
99 widget: Widget::FileInput,
100 initial: None,
101 max_length: None,
102 allow_empty_file: false,
103 max_size: DEFAULT_FILE_MAX_SIZE,
104 }
105 }
106
107 pub fn with_max_size(mut self, max_size: u64) -> Self {
118 self.max_size = max_size;
119 self
120 }
121}
122
123impl FormField for FileField {
124 fn name(&self) -> &str {
125 &self.name
126 }
127
128 fn label(&self) -> Option<&str> {
129 self.label.as_deref()
130 }
131
132 fn required(&self) -> bool {
133 self.required
134 }
135
136 fn help_text(&self) -> Option<&str> {
137 self.help_text.as_deref()
138 }
139
140 fn widget(&self) -> &Widget {
141 &self.widget
142 }
143
144 fn initial(&self) -> Option<&serde_json::Value> {
145 self.initial.as_ref()
146 }
147
148 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
149 match value {
150 None if self.required => Err(FieldError::required(None)),
151 None => Ok(serde_json::Value::Null),
152 Some(v) => {
153 let obj = v
155 .as_object()
156 .ok_or_else(|| FieldError::Invalid("Expected object".to_string()))?;
157
158 let filename = obj
159 .get("filename")
160 .and_then(|f| f.as_str())
161 .ok_or_else(|| FieldError::Invalid("Missing filename".to_string()))?;
162
163 if filename.is_empty() {
164 if self.required {
165 return Err(FieldError::required(None));
166 }
167 return Ok(serde_json::Value::Null);
168 }
169
170 validate_filename_safety(filename)?;
172
173 if let Some(max) = self.max_length
175 && filename.len() > max
176 {
177 return Err(FieldError::Validation(format!(
178 "Filename is too long (max {} characters)",
179 max
180 )));
181 }
182
183 if let Some(size) = obj.get("size").and_then(|s| s.as_u64()) {
185 if size > self.max_size {
186 return Err(FieldError::Validation(format!(
187 "File size {} bytes exceeds maximum allowed size of {} bytes",
188 size, self.max_size
189 )));
190 }
191
192 if !self.allow_empty_file && size == 0 {
194 return Err(FieldError::Validation(
195 "The submitted file is empty".to_string(),
196 ));
197 }
198 } else if !self.allow_empty_file {
199 return Err(FieldError::Validation(
201 "The submitted file is empty".to_string(),
202 ));
203 }
204
205 Ok(v.clone())
206 }
207 }
208 }
209}
210
211pub struct ImageField {
213 pub name: String,
215 pub label: Option<String>,
217 pub required: bool,
219 pub help_text: Option<String>,
221 pub widget: Widget,
223 pub initial: Option<serde_json::Value>,
225 pub max_length: Option<usize>,
227 pub allow_empty_file: bool,
229 pub max_size: u64,
231}
232
233impl ImageField {
234 pub fn new(name: String) -> Self {
246 Self {
247 name,
248 label: None,
249 required: true,
250 help_text: None,
251 widget: Widget::FileInput,
252 initial: None,
253 max_length: None,
254 allow_empty_file: false,
255 max_size: DEFAULT_IMAGE_MAX_SIZE,
256 }
257 }
258
259 pub fn with_max_size(mut self, max_size: u64) -> Self {
270 self.max_size = max_size;
271 self
272 }
273
274 fn is_valid_image_extension(filename: &str) -> bool {
275 let valid_extensions = ["jpg", "jpeg", "png", "gif", "webp", "bmp"];
280 filename
281 .rsplit('.')
282 .next()
283 .map(|ext| valid_extensions.contains(&ext.to_lowercase().as_str()))
284 .unwrap_or(false)
285 }
286}
287
288impl FormField for ImageField {
289 fn name(&self) -> &str {
290 &self.name
291 }
292
293 fn label(&self) -> Option<&str> {
294 self.label.as_deref()
295 }
296
297 fn required(&self) -> bool {
298 self.required
299 }
300
301 fn help_text(&self) -> Option<&str> {
302 self.help_text.as_deref()
303 }
304
305 fn widget(&self) -> &Widget {
306 &self.widget
307 }
308
309 fn initial(&self) -> Option<&serde_json::Value> {
310 self.initial.as_ref()
311 }
312
313 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
314 match value {
315 None if self.required => Err(FieldError::required(None)),
316 None => Ok(serde_json::Value::Null),
317 Some(v) => {
318 let obj = v
319 .as_object()
320 .ok_or_else(|| FieldError::Invalid("Expected object".to_string()))?;
321
322 let filename = obj
323 .get("filename")
324 .and_then(|f| f.as_str())
325 .ok_or_else(|| FieldError::Invalid("Missing filename".to_string()))?;
326
327 if filename.is_empty() {
328 if self.required {
329 return Err(FieldError::required(None));
330 }
331 return Ok(serde_json::Value::Null);
332 }
333
334 validate_filename_safety(filename)?;
336
337 if !Self::is_valid_image_extension(filename) {
339 return Err(FieldError::Validation(
340 "Upload a valid image. The file you uploaded was either not an image or a corrupted image".to_string(),
341 ));
342 }
343
344 if let Some(max) = self.max_length
346 && filename.len() > max
347 {
348 return Err(FieldError::Validation(format!(
349 "Filename is too long (max {} characters)",
350 max
351 )));
352 }
353
354 if let Some(size) = obj.get("size").and_then(|s| s.as_u64()) {
356 if size > self.max_size {
357 return Err(FieldError::Validation(format!(
358 "File size {} bytes exceeds maximum allowed size of {} bytes",
359 size, self.max_size
360 )));
361 }
362
363 if !self.allow_empty_file && size == 0 {
365 return Err(FieldError::Validation(
366 "The submitted file is empty".to_string(),
367 ));
368 }
369 } else if !self.allow_empty_file {
370 return Err(FieldError::Validation(
372 "The submitted file is empty".to_string(),
373 ));
374 }
375
376 Ok(v.clone())
377 }
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use rstest::rstest;
386
387 #[rstest]
394 fn test_filefield_valid() {
395 let field = FileField::new("document".to_string());
397 let file = serde_json::json!({
398 "filename": "test.pdf",
399 "size": 1024
400 });
401
402 let result = field.clean(Some(&file));
404
405 assert!(result.is_ok());
407 }
408
409 #[rstest]
410 fn test_filefield_default_max_size() {
411 let field = FileField::new("document".to_string());
413
414 assert_eq!(field.max_size, 10 * 1024 * 1024);
416 }
417
418 #[rstest]
419 fn test_filefield_custom_max_size() {
420 let field = FileField::new("document".to_string()).with_max_size(5 * 1024 * 1024);
422
423 assert_eq!(field.max_size, 5 * 1024 * 1024);
425 }
426
427 #[rstest]
428 fn test_filefield_within_size_limit() {
429 let field = FileField::new("document".to_string()).with_max_size(1024);
431 let file = serde_json::json!({
432 "filename": "test.pdf",
433 "size": 1024
434 });
435
436 let result = field.clean(Some(&file));
438
439 assert!(result.is_ok());
441 }
442
443 #[rstest]
446 fn test_filefield_exceeds_size_limit() {
447 let field = FileField::new("document".to_string()).with_max_size(1024);
449 let file = serde_json::json!({
450 "filename": "test.pdf",
451 "size": 1025
452 });
453
454 let result = field.clean(Some(&file));
456
457 assert!(
459 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
460 );
461 }
462
463 #[rstest]
464 fn test_filefield_exceeds_default_size_limit() {
465 let field = FileField::new("document".to_string());
467 let over_10mb = 10 * 1024 * 1024 + 1;
468 let file = serde_json::json!({
469 "filename": "huge.bin",
470 "size": over_10mb
471 });
472
473 let result = field.clean(Some(&file));
475
476 assert!(
478 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
479 );
480 }
481
482 #[rstest]
483 fn test_filefield_empty() {
484 let field = FileField::new("document".to_string());
486 let file = serde_json::json!({
487 "filename": "test.pdf",
488 "size": 0
489 });
490
491 assert!(matches!(
493 field.clean(Some(&file)),
494 Err(FieldError::Validation(_))
495 ));
496 }
497
498 #[rstest]
499 fn test_filefield_no_size_field_rejects_when_empty_not_allowed() {
500 let field = FileField::new("document".to_string());
502 let file = serde_json::json!({
503 "filename": "test.pdf"
504 });
505
506 let result = field.clean(Some(&file));
508
509 assert!(matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("empty")));
511 }
512
513 #[rstest]
516 #[case("../../etc/passwd", "directory traversal")]
517 #[case("../secret.txt", "directory traversal")]
518 #[case("foo/../../etc/shadow", "directory traversal")]
519 #[case("..\\windows\\system32", "directory traversal")]
520 #[case("..\\..\\boot.ini", "directory traversal")]
521 fn test_filefield_rejects_directory_traversal(
522 #[case] filename: &str,
523 #[case] expected_msg: &str,
524 ) {
525 let field = FileField::new("document".to_string());
527 let file = serde_json::json!({
528 "filename": filename,
529 "size": 1024
530 });
531
532 let result = field.clean(Some(&file));
534
535 assert!(
537 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains(expected_msg)),
538 "Expected directory traversal rejection for filename: {filename}"
539 );
540 }
541
542 #[rstest]
543 fn test_filefield_rejects_null_bytes() {
544 let field = FileField::new("document".to_string());
546 let file = serde_json::json!({
547 "filename": "file\0name.pdf",
548 "size": 1024
549 });
550
551 let result = field.clean(Some(&file));
553
554 assert!(
556 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("null bytes")),
557 "Expected null bytes rejection"
558 );
559 }
560
561 #[rstest]
562 #[case("/etc/passwd")]
563 #[case("/var/log/syslog")]
564 fn test_filefield_rejects_unix_absolute_path(#[case] filename: &str) {
565 let field = FileField::new("document".to_string());
567 let file = serde_json::json!({
568 "filename": filename,
569 "size": 1024
570 });
571
572 let result = field.clean(Some(&file));
574
575 assert!(
577 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("absolute path")),
578 "Expected absolute path rejection for filename: {filename}"
579 );
580 }
581
582 #[rstest]
583 #[case("C:\\Windows\\system32\\config")]
584 #[case("D:/Documents/secret.txt")]
585 fn test_filefield_rejects_windows_absolute_path(#[case] filename: &str) {
586 let field = FileField::new("document".to_string());
588 let file = serde_json::json!({
589 "filename": filename,
590 "size": 1024
591 });
592
593 let result = field.clean(Some(&file));
595
596 assert!(
598 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("absolute path")),
599 "Expected absolute path rejection for filename: {filename}"
600 );
601 }
602
603 #[rstest]
604 #[case("document.pdf")]
605 #[case("my-file_v2.tar.gz")]
606 #[case("photo (1).jpg")]
607 fn test_filefield_accepts_safe_filenames(#[case] filename: &str) {
608 let field = FileField::new("document".to_string());
610 let file = serde_json::json!({
611 "filename": filename,
612 "size": 1024
613 });
614
615 let result = field.clean(Some(&file));
617
618 assert!(
620 result.is_ok(),
621 "Expected safe filename to be accepted: {filename}"
622 );
623 }
624
625 #[rstest]
628 #[case(1023, true)] #[case(1024, true)] #[case(1025, false)] fn test_filefield_size_boundary(#[case] size: u64, #[case] valid: bool) {
632 let field = FileField::new("document".to_string()).with_max_size(1024);
634 let file = serde_json::json!({
635 "filename": "test.pdf",
636 "size": size
637 });
638
639 assert_eq!(field.clean(Some(&file)).is_ok(), valid);
641 }
642
643 #[rstest]
646 #[case(1024, 512, true)] #[case(1024, 1024, true)] #[case(1024, 2048, false)] #[case(0, 1, false)] fn test_filefield_size_decision_table(
651 #[case] max_size: u64,
652 #[case] file_size: u64,
653 #[case] expected_ok: bool,
654 ) {
655 let field = FileField::new("document".to_string()).with_max_size(max_size);
657 let file = serde_json::json!({
658 "filename": "test.pdf",
659 "size": file_size
660 });
661
662 assert_eq!(field.clean(Some(&file)).is_ok(), expected_ok);
664 }
665
666 #[rstest]
673 fn test_imagefield_valid() {
674 let field = ImageField::new("photo".to_string());
676 let file = serde_json::json!({
677 "filename": "test.jpg",
678 "size": 1024
679 });
680
681 assert!(field.clean(Some(&file)).is_ok());
683 }
684
685 #[rstest]
686 fn test_imagefield_default_max_size() {
687 let field = ImageField::new("photo".to_string());
689
690 assert_eq!(field.max_size, 5 * 1024 * 1024);
692 }
693
694 #[rstest]
695 fn test_imagefield_custom_max_size() {
696 let field = ImageField::new("photo".to_string()).with_max_size(2 * 1024 * 1024);
698
699 assert_eq!(field.max_size, 2 * 1024 * 1024);
701 }
702
703 #[rstest]
706 fn test_imagefield_invalid_extension() {
707 let field = ImageField::new("photo".to_string());
709 let file = serde_json::json!({
710 "filename": "test.pdf",
711 "size": 1024
712 });
713
714 assert!(matches!(
716 field.clean(Some(&file)),
717 Err(FieldError::Validation(_))
718 ));
719 }
720
721 #[rstest]
722 fn test_imagefield_rejects_svg_for_xss_prevention() {
723 let field = ImageField::new("photo".to_string());
725 let svg_file = serde_json::json!({
727 "filename": "malicious.svg",
728 "size": 1024
729 });
730
731 assert!(
733 matches!(field.clean(Some(&svg_file)), Err(FieldError::Validation(_))),
734 "SVG files should be rejected to prevent Stored XSS attacks"
735 );
736 }
737
738 #[rstest]
739 fn test_imagefield_exceeds_size_limit() {
740 let field = ImageField::new("photo".to_string()).with_max_size(1024);
742 let file = serde_json::json!({
743 "filename": "large.jpg",
744 "size": 1025
745 });
746
747 let result = field.clean(Some(&file));
749
750 assert!(
752 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
753 );
754 }
755
756 #[rstest]
757 fn test_imagefield_exceeds_default_size_limit() {
758 let field = ImageField::new("photo".to_string());
760 let over_5mb = 5 * 1024 * 1024 + 1;
761 let file = serde_json::json!({
762 "filename": "huge.png",
763 "size": over_5mb
764 });
765
766 let result = field.clean(Some(&file));
768
769 assert!(
771 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
772 );
773 }
774
775 #[rstest]
778 fn test_imagefield_rejects_directory_traversal() {
779 let field = ImageField::new("photo".to_string());
781 let file = serde_json::json!({
782 "filename": "../../etc/passwd.jpg",
783 "size": 1024
784 });
785
786 let result = field.clean(Some(&file));
788
789 assert!(
791 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("directory traversal")),
792 "Expected directory traversal rejection for ImageField"
793 );
794 }
795
796 #[rstest]
797 fn test_imagefield_rejects_null_bytes() {
798 let field = ImageField::new("photo".to_string());
800 let file = serde_json::json!({
801 "filename": "photo\0.jpg",
802 "size": 1024
803 });
804
805 let result = field.clean(Some(&file));
807
808 assert!(
810 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("null bytes")),
811 "Expected null bytes rejection for ImageField"
812 );
813 }
814
815 #[rstest]
816 fn test_imagefield_rejects_absolute_path() {
817 let field = ImageField::new("photo".to_string());
819 let file = serde_json::json!({
820 "filename": "/etc/photo.jpg",
821 "size": 1024
822 });
823
824 let result = field.clean(Some(&file));
826
827 assert!(
829 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("absolute path")),
830 "Expected absolute path rejection for ImageField"
831 );
832 }
833
834 #[rstest]
837 #[case(2047, true)] #[case(2048, true)] #[case(2049, false)] fn test_imagefield_size_boundary(#[case] size: u64, #[case] valid: bool) {
841 let field = ImageField::new("photo".to_string()).with_max_size(2048);
843 let file = serde_json::json!({
844 "filename": "photo.jpg",
845 "size": size
846 });
847
848 assert_eq!(field.clean(Some(&file)).is_ok(), valid);
850 }
851}