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
9pub struct FileField {
11 pub name: String,
12 pub label: Option<String>,
13 pub required: bool,
14 pub help_text: Option<String>,
15 pub widget: Widget,
16 pub initial: Option<serde_json::Value>,
17 pub max_length: Option<usize>,
18 pub allow_empty_file: bool,
19 pub max_size: u64,
21}
22
23impl FileField {
24 pub fn new(name: String) -> Self {
36 Self {
37 name,
38 label: None,
39 required: true,
40 help_text: None,
41 widget: Widget::FileInput,
42 initial: None,
43 max_length: None,
44 allow_empty_file: false,
45 max_size: DEFAULT_FILE_MAX_SIZE,
46 }
47 }
48
49 pub fn with_max_size(mut self, max_size: u64) -> Self {
60 self.max_size = max_size;
61 self
62 }
63}
64
65impl FormField for FileField {
66 fn name(&self) -> &str {
67 &self.name
68 }
69
70 fn label(&self) -> Option<&str> {
71 self.label.as_deref()
72 }
73
74 fn required(&self) -> bool {
75 self.required
76 }
77
78 fn help_text(&self) -> Option<&str> {
79 self.help_text.as_deref()
80 }
81
82 fn widget(&self) -> &Widget {
83 &self.widget
84 }
85
86 fn initial(&self) -> Option<&serde_json::Value> {
87 self.initial.as_ref()
88 }
89
90 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
91 match value {
92 None if self.required => Err(FieldError::required(None)),
93 None => Ok(serde_json::Value::Null),
94 Some(v) => {
95 let obj = v
97 .as_object()
98 .ok_or_else(|| FieldError::Invalid("Expected object".to_string()))?;
99
100 let filename = obj
101 .get("filename")
102 .and_then(|f| f.as_str())
103 .ok_or_else(|| FieldError::Invalid("Missing filename".to_string()))?;
104
105 if filename.is_empty() {
106 if self.required {
107 return Err(FieldError::required(None));
108 }
109 return Ok(serde_json::Value::Null);
110 }
111
112 if let Some(max) = self.max_length
114 && filename.len() > max
115 {
116 return Err(FieldError::Validation(format!(
117 "Filename is too long (max {} characters)",
118 max
119 )));
120 }
121
122 if let Some(size) = obj.get("size").and_then(|s| s.as_u64()) {
124 if size > self.max_size {
125 return Err(FieldError::Validation(format!(
126 "File size {} bytes exceeds maximum allowed size of {} bytes",
127 size, self.max_size
128 )));
129 }
130
131 if !self.allow_empty_file && size == 0 {
133 return Err(FieldError::Validation(
134 "The submitted file is empty".to_string(),
135 ));
136 }
137 } else if !self.allow_empty_file {
138 return Err(FieldError::Validation(
140 "The submitted file is empty".to_string(),
141 ));
142 }
143
144 Ok(v.clone())
145 }
146 }
147 }
148}
149
150pub struct ImageField {
152 pub name: String,
153 pub label: Option<String>,
154 pub required: bool,
155 pub help_text: Option<String>,
156 pub widget: Widget,
157 pub initial: Option<serde_json::Value>,
158 pub max_length: Option<usize>,
159 pub allow_empty_file: bool,
160 pub max_size: u64,
162}
163
164impl ImageField {
165 pub fn new(name: String) -> Self {
177 Self {
178 name,
179 label: None,
180 required: true,
181 help_text: None,
182 widget: Widget::FileInput,
183 initial: None,
184 max_length: None,
185 allow_empty_file: false,
186 max_size: DEFAULT_IMAGE_MAX_SIZE,
187 }
188 }
189
190 pub fn with_max_size(mut self, max_size: u64) -> Self {
201 self.max_size = max_size;
202 self
203 }
204
205 fn is_valid_image_extension(filename: &str) -> bool {
206 let valid_extensions = ["jpg", "jpeg", "png", "gif", "webp", "bmp"];
211 filename
212 .rsplit('.')
213 .next()
214 .map(|ext| valid_extensions.contains(&ext.to_lowercase().as_str()))
215 .unwrap_or(false)
216 }
217}
218
219impl FormField for ImageField {
220 fn name(&self) -> &str {
221 &self.name
222 }
223
224 fn label(&self) -> Option<&str> {
225 self.label.as_deref()
226 }
227
228 fn required(&self) -> bool {
229 self.required
230 }
231
232 fn help_text(&self) -> Option<&str> {
233 self.help_text.as_deref()
234 }
235
236 fn widget(&self) -> &Widget {
237 &self.widget
238 }
239
240 fn initial(&self) -> Option<&serde_json::Value> {
241 self.initial.as_ref()
242 }
243
244 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
245 match value {
246 None if self.required => Err(FieldError::required(None)),
247 None => Ok(serde_json::Value::Null),
248 Some(v) => {
249 let obj = v
250 .as_object()
251 .ok_or_else(|| FieldError::Invalid("Expected object".to_string()))?;
252
253 let filename = obj
254 .get("filename")
255 .and_then(|f| f.as_str())
256 .ok_or_else(|| FieldError::Invalid("Missing filename".to_string()))?;
257
258 if filename.is_empty() {
259 if self.required {
260 return Err(FieldError::required(None));
261 }
262 return Ok(serde_json::Value::Null);
263 }
264
265 if !Self::is_valid_image_extension(filename) {
267 return Err(FieldError::Validation(
268 "Upload a valid image. The file you uploaded was either not an image or a corrupted image".to_string(),
269 ));
270 }
271
272 if let Some(max) = self.max_length
274 && filename.len() > max
275 {
276 return Err(FieldError::Validation(format!(
277 "Filename is too long (max {} characters)",
278 max
279 )));
280 }
281
282 if let Some(size) = obj.get("size").and_then(|s| s.as_u64()) {
284 if size > self.max_size {
285 return Err(FieldError::Validation(format!(
286 "File size {} bytes exceeds maximum allowed size of {} bytes",
287 size, self.max_size
288 )));
289 }
290
291 if !self.allow_empty_file && size == 0 {
293 return Err(FieldError::Validation(
294 "The submitted file is empty".to_string(),
295 ));
296 }
297 } else if !self.allow_empty_file {
298 return Err(FieldError::Validation(
300 "The submitted file is empty".to_string(),
301 ));
302 }
303
304 Ok(v.clone())
305 }
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use rstest::rstest;
314
315 #[rstest]
322 fn test_filefield_valid() {
323 let field = FileField::new("document".to_string());
325 let file = serde_json::json!({
326 "filename": "test.pdf",
327 "size": 1024
328 });
329
330 let result = field.clean(Some(&file));
332
333 assert!(result.is_ok());
335 }
336
337 #[rstest]
338 fn test_filefield_default_max_size() {
339 let field = FileField::new("document".to_string());
341
342 assert_eq!(field.max_size, 10 * 1024 * 1024);
344 }
345
346 #[rstest]
347 fn test_filefield_custom_max_size() {
348 let field = FileField::new("document".to_string()).with_max_size(5 * 1024 * 1024);
350
351 assert_eq!(field.max_size, 5 * 1024 * 1024);
353 }
354
355 #[rstest]
356 fn test_filefield_within_size_limit() {
357 let field = FileField::new("document".to_string()).with_max_size(1024);
359 let file = serde_json::json!({
360 "filename": "test.pdf",
361 "size": 1024
362 });
363
364 let result = field.clean(Some(&file));
366
367 assert!(result.is_ok());
369 }
370
371 #[rstest]
374 fn test_filefield_exceeds_size_limit() {
375 let field = FileField::new("document".to_string()).with_max_size(1024);
377 let file = serde_json::json!({
378 "filename": "test.pdf",
379 "size": 1025
380 });
381
382 let result = field.clean(Some(&file));
384
385 assert!(
387 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
388 );
389 }
390
391 #[rstest]
392 fn test_filefield_exceeds_default_size_limit() {
393 let field = FileField::new("document".to_string());
395 let over_10mb = 10 * 1024 * 1024 + 1;
396 let file = serde_json::json!({
397 "filename": "huge.bin",
398 "size": over_10mb
399 });
400
401 let result = field.clean(Some(&file));
403
404 assert!(
406 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
407 );
408 }
409
410 #[rstest]
411 fn test_filefield_empty() {
412 let field = FileField::new("document".to_string());
414 let file = serde_json::json!({
415 "filename": "test.pdf",
416 "size": 0
417 });
418
419 assert!(matches!(
421 field.clean(Some(&file)),
422 Err(FieldError::Validation(_))
423 ));
424 }
425
426 #[rstest]
427 fn test_filefield_no_size_field_rejects_when_empty_not_allowed() {
428 let field = FileField::new("document".to_string());
430 let file = serde_json::json!({
431 "filename": "test.pdf"
432 });
433
434 let result = field.clean(Some(&file));
436
437 assert!(matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("empty")));
439 }
440
441 #[rstest]
444 #[case(1023, true)] #[case(1024, true)] #[case(1025, false)] fn test_filefield_size_boundary(#[case] size: u64, #[case] valid: bool) {
448 let field = FileField::new("document".to_string()).with_max_size(1024);
450 let file = serde_json::json!({
451 "filename": "test.pdf",
452 "size": size
453 });
454
455 assert_eq!(field.clean(Some(&file)).is_ok(), valid);
457 }
458
459 #[rstest]
462 #[case(1024, 512, true)] #[case(1024, 1024, true)] #[case(1024, 2048, false)] #[case(0, 1, false)] fn test_filefield_size_decision_table(
467 #[case] max_size: u64,
468 #[case] file_size: u64,
469 #[case] expected_ok: bool,
470 ) {
471 let field = FileField::new("document".to_string()).with_max_size(max_size);
473 let file = serde_json::json!({
474 "filename": "test.pdf",
475 "size": file_size
476 });
477
478 assert_eq!(field.clean(Some(&file)).is_ok(), expected_ok);
480 }
481
482 #[rstest]
489 fn test_imagefield_valid() {
490 let field = ImageField::new("photo".to_string());
492 let file = serde_json::json!({
493 "filename": "test.jpg",
494 "size": 1024
495 });
496
497 assert!(field.clean(Some(&file)).is_ok());
499 }
500
501 #[rstest]
502 fn test_imagefield_default_max_size() {
503 let field = ImageField::new("photo".to_string());
505
506 assert_eq!(field.max_size, 5 * 1024 * 1024);
508 }
509
510 #[rstest]
511 fn test_imagefield_custom_max_size() {
512 let field = ImageField::new("photo".to_string()).with_max_size(2 * 1024 * 1024);
514
515 assert_eq!(field.max_size, 2 * 1024 * 1024);
517 }
518
519 #[rstest]
522 fn test_imagefield_invalid_extension() {
523 let field = ImageField::new("photo".to_string());
525 let file = serde_json::json!({
526 "filename": "test.pdf",
527 "size": 1024
528 });
529
530 assert!(matches!(
532 field.clean(Some(&file)),
533 Err(FieldError::Validation(_))
534 ));
535 }
536
537 #[rstest]
538 fn test_imagefield_rejects_svg_for_xss_prevention() {
539 let field = ImageField::new("photo".to_string());
541 let svg_file = serde_json::json!({
543 "filename": "malicious.svg",
544 "size": 1024
545 });
546
547 assert!(
549 matches!(field.clean(Some(&svg_file)), Err(FieldError::Validation(_))),
550 "SVG files should be rejected to prevent Stored XSS attacks"
551 );
552 }
553
554 #[rstest]
555 fn test_imagefield_exceeds_size_limit() {
556 let field = ImageField::new("photo".to_string()).with_max_size(1024);
558 let file = serde_json::json!({
559 "filename": "large.jpg",
560 "size": 1025
561 });
562
563 let result = field.clean(Some(&file));
565
566 assert!(
568 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
569 );
570 }
571
572 #[rstest]
573 fn test_imagefield_exceeds_default_size_limit() {
574 let field = ImageField::new("photo".to_string());
576 let over_5mb = 5 * 1024 * 1024 + 1;
577 let file = serde_json::json!({
578 "filename": "huge.png",
579 "size": over_5mb
580 });
581
582 let result = field.clean(Some(&file));
584
585 assert!(
587 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("exceeds maximum"))
588 );
589 }
590
591 #[rstest]
594 #[case(2047, true)] #[case(2048, true)] #[case(2049, false)] fn test_imagefield_size_boundary(#[case] size: u64, #[case] valid: bool) {
598 let field = ImageField::new("photo".to_string()).with_max_size(2048);
600 let file = serde_json::json!({
601 "filename": "photo.jpg",
602 "size": size
603 });
604
605 assert_eq!(field.clean(Some(&file)).is_ok(), valid);
607 }
608}