Skip to main content

foxtive_ntex_multipart/
file_validator.rs

1use crate::result::MultipartResult;
2use crate::{FileInput, MultipartError};
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5
6#[derive(Debug, Clone)]
7pub struct InputError {
8    pub name: String,
9    pub error: ErrorMessage,
10}
11
12#[derive(Debug, Clone, Eq, PartialEq)]
13pub enum ErrorMessage {
14    NoFiles(String),
15    FileTooSmall(String, String, usize),
16    FileTooLarge(String, String, usize),
17    TooFewFiles(String, usize),
18    TooManyFiles(String, usize),
19    InvalidFileExtension(String, String, Option<String>),
20    InvalidContentType(String, String, String),
21    MissingFileExtension(String, String, String),
22}
23
24#[derive(Debug, Clone, Default)]
25pub struct Validator {
26    rules: HashMap<String, FileRules>,
27}
28
29// Struct for File Validation Rules
30#[derive(Debug, Default, Clone)]
31pub struct FileRules {
32    /// Whether field is required
33    pub required: bool,
34
35    /// Whether file extension is required
36    pub extension_required: bool,
37
38    /// Min file size in bytes
39    pub min_size: Option<usize>,
40
41    /// Max file size in bytes
42    pub max_size: Option<usize>,
43
44    /// Allowed file extensions
45    pub allowed_extensions: Option<Vec<String>>,
46
47    /// Allowed content types
48    pub allowed_content_types: Option<Vec<String>>,
49
50    /// Min number of files, this only works when validating through `Multipart` struct
51    pub min_files: Option<usize>,
52
53    /// Max number of files, this only works when validating through `Multipart` struct
54    pub max_files: Option<usize>,
55}
56
57impl Validator {
58    pub fn new() -> Self {
59        Default::default()
60    }
61
62    pub fn add_rule(&mut self, field: &str, rules: FileRules) -> Self {
63        let mut validator = self.clone();
64        validator.rules.insert(field.to_string(), rules);
65        validator
66    }
67
68    pub fn validate(&self, files: &HashMap<String, Vec<FileInput>>) -> MultipartResult<()> {
69        for (field_name, rules) in &self.rules {
70            let files = files.get(field_name);
71            Self::validate_files(field_name.clone(), files, rules)
72                .map_err(MultipartError::ValidationError)?;
73        }
74
75        Ok(())
76    }
77
78    fn validate_files(
79        field_name: String,
80        files: Option<&Vec<FileInput>>,
81        rules: &FileRules,
82    ) -> Result<(), InputError> {
83        if files.is_none() {
84            if rules.required {
85                return Err(InputError {
86                    name: field_name.clone(),
87                    error: ErrorMessage::NoFiles(field_name),
88                });
89            }
90
91            return Ok(());
92        }
93
94        let files = files.unwrap();
95        let file_count = files.len();
96
97        // Validate required
98        if rules.required && file_count == 0 {
99            return Err(InputError {
100                name: field_name.clone(),
101                error: ErrorMessage::NoFiles(field_name),
102            });
103        }
104
105        if file_count < rules.min_files.unwrap_or(0) {
106            return Err(InputError {
107                name: field_name.clone(),
108                error: ErrorMessage::TooFewFiles(field_name, file_count),
109            });
110        }
111
112        if file_count > rules.max_files.unwrap_or(usize::MAX) {
113            return Err(InputError {
114                name: field_name.clone(),
115                error: ErrorMessage::TooManyFiles(field_name, file_count),
116            });
117        }
118
119        for file in files {
120            Self::validate_file(&field_name, rules.clone(), file)?;
121        }
122
123        // If all checks passed
124        Ok(())
125    }
126
127    fn validate_file(
128        field_name: &str,
129        rule: FileRules,
130        file: &FileInput,
131    ) -> Result<(), InputError> {
132        // Validate file extension
133        if rule.extension_required && file.extension.is_none() {
134            return Err(InputError {
135                name: field_name.to_string(),
136                error: ErrorMessage::MissingFileExtension(
137                    field_name.to_string(),
138                    file.file_name.clone(),
139                    "Extension is required".to_string(),
140                ),
141            });
142        }
143
144        // Validate file size
145        if let Some(min_size) = rule.min_size
146            && file.size < min_size
147        {
148            return Err(InputError {
149                name: field_name.to_string(),
150                error: ErrorMessage::FileTooSmall(
151                    field_name.to_string(),
152                    file.file_name.clone(),
153                    min_size,
154                ),
155            });
156        }
157
158        if let Some(max_size) = rule.max_size
159            && file.size > max_size
160        {
161            return Err(InputError {
162                name: field_name.to_string(),
163                error: ErrorMessage::FileTooLarge(
164                    field_name.to_string(),
165                    file.file_name.clone(),
166                    max_size,
167                ),
168            });
169        }
170
171        // Validate file extension
172        if let Some(allowed_extensions) = &rule.allowed_extensions {
173            if let Some(extension) = &file.extension {
174                if !allowed_extensions.contains(&extension.to_lowercase()) {
175                    return Err(InputError {
176                        name: field_name.to_string(),
177                        error: ErrorMessage::InvalidFileExtension(
178                            field_name.to_string(),
179                            file.file_name.clone(),
180                            file.extension.clone(),
181                        ),
182                    });
183                }
184            } else {
185                return Err(InputError {
186                    name: field_name.to_string(),
187                    error: ErrorMessage::MissingFileExtension(
188                        field_name.to_string(),
189                        file.file_name.clone(),
190                        "File extension is missing but required".to_string(),
191                    ),
192                });
193            }
194        }
195
196        // Validate content type
197        if let Some(allowed_content_types) = &rule.allowed_content_types
198            && !allowed_content_types.contains(&file.content_type.to_lowercase())
199        {
200            return Err(InputError {
201                name: field_name.to_string(),
202                error: ErrorMessage::InvalidContentType(
203                    field_name.to_string(),
204                    file.file_name.clone(),
205                    format!(
206                        "Invalid content type. Allowed content types are: {allowed_content_types:?}"
207                    ),
208                ),
209            });
210        }
211
212        Ok(())
213    }
214}
215
216impl Display for ErrorMessage {
217    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218        match self {
219            ErrorMessage::NoFiles(field_name) => {
220                let display_name = field_name.replace("_", " ");
221                write!(f, "No files were uploaded for field: '{display_name}'")
222            }
223            ErrorMessage::FileTooSmall(field_name, file_name, size) => {
224                let display_name = field_name.replace("_", " ");
225                write!(
226                    f,
227                    "File '{}' is too small for field '{}'. Minimum size is {}",
228                    file_name,
229                    display_name,
230                    FileInput::format_size(*size)
231                )
232            }
233            ErrorMessage::FileTooLarge(field_name, file_name, size) => {
234                let display_name = field_name.replace("_", " ");
235                write!(
236                    f,
237                    "File '{}' is too large for field '{}'. Maximum size is {}",
238                    file_name,
239                    display_name,
240                    FileInput::format_size(*size)
241                )
242            }
243            ErrorMessage::TooFewFiles(field_name, count) => {
244                let display_name = field_name.replace("_", " ");
245                write!(
246                    f,
247                    "Too few files uploaded for field '{}'. Current count: {}, minimum required",
248                    display_name, count
249                )
250            }
251            ErrorMessage::TooManyFiles(field_name, count) => {
252                let display_name = field_name.replace("_", " ");
253                write!(
254                    f,
255                    "Too many files uploaded for field '{}'. Current count: {}, maximum allowed",
256                    display_name, count
257                )
258            }
259            ErrorMessage::InvalidFileExtension(field_name, file_name, ext) => {
260                let display_name = field_name.replace("_", " ");
261                match ext {
262                    Some(extension) => write!(
263                        f,
264                        "Invalid file extension '.{}' for file '{}' in field '{}'",
265                        extension, file_name, display_name
266                    ),
267                    None => write!(
268                        f,
269                        "Missing file extension for file '{}' in field '{}'",
270                        file_name, display_name
271                    ),
272                }
273            }
274            ErrorMessage::InvalidContentType(field_name, file_name, message) => {
275                let display_name = field_name.replace("_", " ");
276                write!(
277                    f,
278                    "Invalid content type for file '{}' in field '{}': {}",
279                    file_name, display_name, message
280                )
281            }
282            ErrorMessage::MissingFileExtension(field_name, file_name, message) => {
283                let display_name = field_name.replace("_", " ");
284                write!(
285                    f,
286                    "Missing file extension for file '{}' in field '{}': {}",
287                    file_name, display_name, message
288                )
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::MultipartError;
298
299    // Helper function to create a file input
300    fn create_file_input(
301        field_name: &str,
302        file_name: &str,
303        size: usize,
304        extension: Option<&str>,
305        content_type: &str,
306    ) -> FileInput {
307        FileInput {
308            field_name: field_name.to_string(),
309            file_name: file_name.to_string(),
310            size,
311            extension: extension.map(|e| e.to_string()),
312            content_type: content_type.to_string(),
313            ..Default::default()
314        }
315    }
316
317    #[test]
318    fn test_validate_required_files_missing() {
319        let validator = Validator::new().add_rule(
320            "file_field",
321            FileRules {
322                required: true,
323                ..Default::default()
324            },
325        );
326
327        let mut files = HashMap::new();
328        files.insert("file_field".to_string(), vec![]);
329
330        let result = validator.validate(&files);
331
332        assert!(result.is_err());
333        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
334            assert_eq!(error, ErrorMessage::NoFiles("file_field".to_string()));
335        }
336    }
337
338    #[test]
339    fn test_validate_required_files_present() {
340        let validator = Validator::new().add_rule(
341            "file_field",
342            FileRules {
343                required: true,
344                ..Default::default()
345            },
346        );
347
348        let mut files = HashMap::new();
349        let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
350        files.insert("file_field".to_string(), vec![file]);
351
352        let result = validator.validate(&files);
353
354        assert!(result.is_ok());
355    }
356
357    #[test]
358    fn test_validate_file_size_too_small() {
359        let validator = Validator::new().add_rule(
360            "file_field",
361            FileRules {
362                min_size: Some(1024),
363                ..Default::default()
364            },
365        );
366
367        let mut files = HashMap::new();
368        let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
369        files.insert("file_field".to_string(), vec![file]);
370
371        let result = validator.validate(&files);
372
373        assert!(result.is_err());
374        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
375            assert_eq!(
376                error,
377                ErrorMessage::FileTooSmall("file_field".to_string(), "test.jpg".to_string(), 1024)
378            );
379        }
380    }
381
382    #[test]
383    fn test_validate_file_size_ok() {
384        let validator = Validator::new().add_rule(
385            "file_field",
386            FileRules {
387                min_size: Some(100),
388                max_size: Some(1024),
389                ..Default::default()
390            },
391        );
392
393        let mut files = HashMap::new();
394        let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
395        files.insert("file_field".to_string(), vec![file]);
396
397        let result = validator.validate(&files);
398
399        assert!(result.is_ok());
400    }
401
402    #[test]
403    fn test_validate_file_extension_invalid() {
404        let validator = Validator::new().add_rule(
405            "file_field",
406            FileRules {
407                allowed_extensions: Some(vec!["jpg".to_string(), "png".to_string()]),
408                ..Default::default()
409            },
410        );
411
412        let mut files = HashMap::new();
413        let file = create_file_input("file_field", "test.txt", 500, Some("txt"), "image/jpeg");
414        files.insert("file_field".to_string(), vec![file]);
415
416        let result = validator.validate(&files);
417
418        assert!(result.is_err());
419        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
420            assert_eq!(
421                error,
422                ErrorMessage::InvalidFileExtension(
423                    "file_field".to_string(),
424                    "test.txt".to_string(),
425                    Some("txt".to_string())
426                )
427            );
428        }
429    }
430
431    #[test]
432    fn test_validate_file_extension_valid() {
433        let validator = Validator::new().add_rule(
434            "file_field",
435            FileRules {
436                allowed_extensions: Some(vec!["jpg".to_string(), "png".to_string()]),
437                ..Default::default()
438            },
439        );
440
441        let mut files = HashMap::new();
442        let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
443        files.insert("file_field".to_string(), vec![file]);
444
445        let result = validator.validate(&files);
446
447        assert!(result.is_ok());
448    }
449
450    #[test]
451    fn test_validate_content_type_invalid() {
452        let validator = Validator::new().add_rule(
453            "file_field",
454            FileRules {
455                allowed_content_types: Some(vec![
456                    "image/jpeg".to_string(),
457                    "image/png".to_string(),
458                ]),
459                ..Default::default()
460            },
461        );
462
463        let mut files = HashMap::new();
464        let file = create_file_input(
465            "file_field",
466            "test.jpg",
467            500,
468            Some("jpg"),
469            "application/pdf",
470        );
471        files.insert("file_field".to_string(), vec![file]);
472
473        let result = validator.validate(&files);
474
475        assert!(result.is_err());
476        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
477            assert_eq!(
478                error,
479                ErrorMessage::InvalidContentType(
480                    "file_field".to_string(),
481                    "test.jpg".to_string(),
482                    "Invalid content type. Allowed content types are: [\"image/jpeg\", \"image/png\"]".to_string()
483                )
484            );
485        }
486    }
487
488    #[test]
489    fn test_validate_file_count_too_few() {
490        let validator = Validator::new().add_rule(
491            "file_field",
492            FileRules {
493                min_files: Some(2),
494                ..Default::default()
495            },
496        );
497
498        let mut files = HashMap::new();
499        let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
500        files.insert("file_field".to_string(), vec![file]);
501
502        let result = validator.validate(&files);
503
504        assert!(result.is_err());
505        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
506            assert_eq!(
507                error,
508                ErrorMessage::TooFewFiles("file_field".to_string(), 1)
509            );
510        }
511    }
512
513    #[test]
514    fn test_validate_file_count_too_many() {
515        let validator = Validator::new().add_rule(
516            "file_field",
517            FileRules {
518                max_files: Some(1),
519                ..Default::default()
520            },
521        );
522
523        let mut files = HashMap::new();
524        let file1 = create_file_input("file_field", "test1.jpg", 500, Some("jpg"), "image/jpeg");
525        let file2 = create_file_input("file_field", "test2.jpg", 500, Some("jpg"), "image/jpeg");
526        files.insert("file_field".to_string(), vec![file1, file2]);
527
528        let result = validator.validate(&files);
529
530        assert!(result.is_err());
531        if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
532            assert_eq!(
533                error,
534                ErrorMessage::TooManyFiles("file_field".to_string(), 2)
535            );
536        }
537    }
538
539    #[test]
540    fn test_validate_file_count_ok() {
541        let validator = Validator::new().add_rule(
542            "file_field",
543            FileRules {
544                max_files: Some(2),
545                min_files: Some(1),
546                ..Default::default()
547            },
548        );
549
550        let mut files = HashMap::new();
551        let file1 = create_file_input("file_field", "test1.jpg", 500, Some("jpg"), "image/jpeg");
552        let file2 = create_file_input("file_field", "test2.jpg", 500, Some("jpg"), "image/jpeg");
553        files.insert("file_field".to_string(), vec![file1, file2]);
554
555        let result = validator.validate(&files);
556
557        assert!(result.is_ok());
558    }
559}