Skip to main content

sz_rust_core/upload/
validate.rs

1//! 文件上传校验模块 — 对齐 PHP `think\Validate` 文件校验规则 + `app\common\library\storage\Driver::validate`
2//!
3//! 本模块实现文件类型/大小校验机制,对齐 PHP:
4//! - `think\Validate::fileSize` / `fileExt` / `fileMime` 三个校验规则
5//!   (vendor `framework/src/think/Validate.php` 第 957-1062 行)
6//! - `app\common\library\storage\Driver::validate` 业务层校验驱动
7//!   (第 25-46 行,默认规则 + 自定义消息)
8//! - `app\api\controller\file\Upload.php` 文件类型分类逻辑
9//!   (第 62-69 行,image/video/file 三类)
10//!
11//! ## PHP 对齐
12//!
13//! ### 核心类映射
14//!
15//! | PHP 类/方法 | Rust 结构/方法 | 说明 |
16//! |-------------|---------------|------|
17//! | `Driver::validate($name, $fileInfo, $sence)` | [`FileValidator::validate_image`] | 图片校验驱动 |
18//! | `validate([$name=>['fileSize'=>...]])` | [`FileValidateRule`] | 校验规则 |
19//! | `$name.'.fileSize' => '最大可上传2M图片'` | [`FileValidateMessages`] | 校验消息 |
20//! | `Validate::fileExt()` / `checkExt()` | [`FileValidator::check_ext`] | 扩展名校验 |
21//! | `Validate::fileMime()` / `checkMime()` | [`FileValidator::check_mime`] | MIME 校验 |
22//! | `Validate::fileSize()` / `checkSize()` | [`FileValidator::check_size`] | 大小校验 |
23//! | `in_array($extension, [...])` 文件类型分类 | [`detect_file_type`] | 文件类型检测 |
24//!
25//! ### PHP 行为对齐(R5 硬约束)
26//!
27//! - **R5-9**:`checkExt` 使用 `strtolower($file->extension())` + `in_array($ext, explode(',', $rule))`
28//!   (对齐 PHP Validate.php 第 957-964 行)
29//! - **R5-10**:`checkMime` 使用 `strtolower($file->getMime())` + `in_array($mime, explode(',', $rule))`
30//!   (对齐 PHP Validate.php 第 985-992 行)
31//! - **R5-11**:`checkSize` 使用 `$file->getSize() <= (int) $size`
32//!   (对齐 PHP Validate.php 第 973-976 行)
33//! - **R5-12**:`Driver::validate` 仅 `sence == 'image'` 执行校验
34//!   (对齐 PHP Driver.php 第 26 行)
35//! - **R5-13**:默认图片规则 `fileSize=20971520, fileExt='jpg,jpeg,png,gif,bmp', fileMime='image/jpeg,image/png,image/gif,image/bmp'`
36//!   (对齐 PHP Driver.php 第 30-32 行)
37//! - **R5-14**:默认图片消息 `'最大可上传2M图片'` / `'只能上传jpg,jpeg,png,gif,bmp格式图片'`
38//!   (对齐 PHP Driver.php 第 34-38 行)
39//! - **R5-15**:文件类型分类 image 12 种 / video 13 种 / file 其他
40//!   (对齐 PHP Upload.php 第 62-69 行)
41//!
42//! ## PHP 源码参考
43//!
44//! - `e:\vue\test\鲜视达\server\vendor\topthink\framework\src\think\Validate.php`
45//!   - 第 957-964 行:`checkExt(File $file, $ext)`
46//!   - 第 973-976 行:`checkSize(File $file, $size)`
47//!   - 第 985-992 行:`checkMime(File $file, $mime)`
48//!   - 第 1002-1016 行:`fileExt($file, $rule)`
49//!   - 第 1025-1039 行:`fileMime($file, $rule)`
50//!   - 第 1048-1062 行:`fileSize($file, $rule)`
51//! - `e:\vue\test\鲜视达\server\app\common\library\storage\Driver.php`
52//!   - 第 25-46 行:`validate($name, $fileInfo, $sence = 'image')`
53//! - `e:\vue\test\鲜视达\server\app\api\controller\file\Upload.php`
54//!   - 第 62-69 行:文件类型分类逻辑
55
56use super::{File, UploadedFile};
57
58// ============================================================================
59// 错误类型
60// ============================================================================
61
62/// 文件校验错误 — 对齐 PHP `Driver::validate` 校验失败
63///
64/// PHP 行为:校验失败时抛出异常,`Driver::validate` 捕获后设置 `$this->engine->error` 并返回 `false`。
65/// Rust 端使用 `Result<(), FileValidateError>` 表达校验失败。
66#[derive(Debug, thiserror::Error)]
67pub enum FileValidateError {
68    /// 文件大小超过限制(对齐 PHP `fileSize` 规则失败)
69    ///
70    /// 错误消息对齐 PHP `Driver.php` 第 35 行:`'最大可上传2M图片'`
71    #[error("{msg}")]
72    SizeExceeded {
73        /// 实际大小(字节)
74        actual: u64,
75        /// 最大允许大小(字节)
76        max: u64,
77        /// 错误消息(对齐 PHP 自定义消息)
78        msg: String,
79    },
80
81    /// 扩展名不允许(对齐 PHP `fileExt` 规则失败)
82    ///
83    /// 错误消息对齐 PHP `Driver.php` 第 36 行:`'只能上传jpg,jpeg,png,gif,bmp格式图片'`
84    #[error("{msg}")]
85    ExtNotAllowed {
86        /// 实际扩展名(小写)
87        ext: String,
88        /// 允许的扩展名列表
89        allowed: Vec<String>,
90        /// 错误消息
91        msg: String,
92    },
93
94    /// MIME 类型不允许(对齐 PHP `fileMime` 规则失败)
95    ///
96    /// 错误消息对齐 PHP `Driver.php` 第 37 行:`'只能上传jpg,jpeg,png,gif,bmp格式图片'`
97    #[error("{msg}")]
98    MimeNotAllowed {
99        /// 实际 MIME 类型(小写)
100        mime: String,
101        /// 允许的 MIME 列表
102        allowed: Vec<String>,
103        /// 错误消息
104        msg: String,
105    },
106
107    /// 获取文件信息失败(IO 错误)
108    #[error(transparent)]
109    Io(#[from] std::io::Error),
110
111    /// 获取 MIME 失败(对齐 PHP `finfo_file` 失败)
112    #[error("获取 MIME 失败: {0}")]
113    MimeDetect(String),
114}
115
116// ============================================================================
117// 校验规则
118// ============================================================================
119
120/// 文件校验规则 — 对齐 PHP `Driver::validate` 的 `fileSize`/`fileExt`/`fileMime` 规则
121///
122/// PHP 原始规则(`app/common/library/storage/Driver.php` 第 29-33 行):
123/// ```php
124/// validate([$name=>[
125///     'fileSize' => 20971520,
126///     'fileExt' => 'jpg,jpeg,png,gif,bmp',
127///     'fileMime' => 'image/jpeg,image/png,image/gif,image/bmp',
128/// ]])
129/// ```
130///
131/// ## 字段语义
132///
133/// - `file_size`:`None` 表示不校验大小(对齐 PHP 未设置 `fileSize` 规则)
134/// - `file_ext`:`None` 表示不校验扩展名
135/// - `file_mime`:`None` 表示不校验 MIME
136#[derive(Debug, Clone, Default)]
137pub struct FileValidateRule {
138    /// 最大文件大小(字节),None 表示不校验
139    pub file_size: Option<u64>,
140    /// 扩展名白名单(小写),None 表示不校验
141    pub file_ext: Option<Vec<String>>,
142    /// MIME 白名单(小写),None 表示不校验
143    pub file_mime: Option<Vec<String>>,
144}
145
146impl FileValidateRule {
147    /// 创建空规则(不校验任何项)
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    /// 设置最大文件大小(对齐 PHP `'fileSize' => 20971520`)
153    pub fn with_size(mut self, size: u64) -> Self {
154        self.file_size = Some(size);
155        self
156    }
157
158    /// 设置扩展名白名单(从逗号分隔字符串解析,对齐 PHP `explode(',', $ext)` + `strtolower`)
159    pub fn with_ext(mut self, ext: &str) -> Self {
160        self.file_ext = Some(parse_ext_list(ext));
161        self
162    }
163
164    /// 设置扩展名白名单(从 Vec,自动 lowercase)
165    pub fn with_ext_vec(mut self, ext: Vec<String>) -> Self {
166        self.file_ext = Some(ext.into_iter().map(|e| e.to_lowercase()).collect());
167        self
168    }
169
170    /// 设置 MIME 白名单(从逗号分隔字符串解析)
171    pub fn with_mime(mut self, mime: &str) -> Self {
172        self.file_mime = Some(parse_mime_list(mime));
173        self
174    }
175
176    /// 设置 MIME 白名单(从 Vec,自动 lowercase)
177    pub fn with_mime_vec(mut self, mime: Vec<String>) -> Self {
178        self.file_mime = Some(mime.into_iter().map(|m| m.to_lowercase()).collect());
179        self
180    }
181
182    /// 默认图片校验规则 — 对齐 PHP `Driver::validate` 第 30-32 行
183    ///
184    /// PHP 原始规则:
185    /// ```php
186    /// 'fileSize' => 20971520, // 20MB(PHP 注释错误标为 "2M")
187    /// 'fileExt' => 'jpg,jpeg,png,gif,bmp',
188    /// 'fileMime' => 'image/jpeg,image/png,image/gif,image/bmp',
189    /// ```
190    pub fn default_image() -> Self {
191        Self::new()
192            .with_size(20 * 1024 * 1024)
193            .with_ext("jpg,jpeg,png,gif,bmp")
194            .with_mime("image/jpeg,image/png,image/gif,image/bmp")
195    }
196}
197
198// ============================================================================
199// 校验消息
200// ============================================================================
201
202/// 文件校验消息 — 对齐 PHP `Driver::validate` 第 34-38 行自定义消息
203///
204/// PHP 原始消息:
205/// ```php
206/// [
207///     $name.'.fileSize' => '最大可上传2M图片',
208///     $name.'.fileExt' => '只能上传jpg,jpeg,png,gif,bmp格式图片',
209///     $name.'.fileMime' => '只能上传jpg,jpeg,png,gif,bmp格式图片'
210/// ]
211/// ```
212#[derive(Debug, Clone)]
213pub struct FileValidateMessages {
214    /// 大小校验失败消息
215    pub file_size: String,
216    /// 扩展名校验失败消息
217    pub file_ext: String,
218    /// MIME 校验失败消息
219    pub file_mime: String,
220}
221
222impl Default for FileValidateMessages {
223    /// 默认消息 — 对齐 PHP `Validate.php` 第 110-112 行 `$typeMsg` + `zh-cn.php` 第 90-92 行翻译
224    ///
225    /// PHP 默认消息(中文环境):
226    /// - `fileSize` → `'上传文件大小不符!'`
227    /// - `fileExt` → `'上传文件后缀不允许'`
228    /// - `fileMime` → `'上传文件MIME类型不允许!'`
229    fn default() -> Self {
230        Self {
231            file_size: "上传文件大小不符!".to_string(),
232            file_ext: "上传文件后缀不允许".to_string(),
233            file_mime: "上传文件MIME类型不允许!".to_string(),
234        }
235    }
236}
237
238impl FileValidateMessages {
239    /// 默认图片校验消息 — 对齐 PHP `Driver::validate` 第 34-38 行
240    pub fn default_image() -> Self {
241        Self {
242            file_size: "最大可上传2M图片".to_string(),
243            file_ext: "只能上传jpg,jpeg,png,gif,bmp格式图片".to_string(),
244            file_mime: "只能上传jpg,jpeg,png,gif,bmp格式图片".to_string(),
245        }
246    }
247}
248
249// ============================================================================
250// 文件校验器
251// ============================================================================
252
253/// 文件校验器 — 对齐 PHP `app\common\library\storage\Driver::validate`
254///
255/// PHP 行为(第 25-46 行):
256/// ```php
257/// public function validate($name, $fileInfo, $sence = 'image'){
258///     if($sence == 'image'){
259///         try{
260///             validate([$name=>[...]], [$name.'.fileSize' => '...', ...])
261///                 ->check([$name => $fileInfo]);
262///             return true;
263///         }catch(\Exception $e){
264///             $this->engine->error = $e->getMessage();
265///             return false;
266///         }
267///     }
268///     return false;
269/// }
270/// ```
271///
272/// ## Rust 端语义
273///
274/// - PHP `sence == 'image'` 分支 → [`FileValidator::validate_image`]
275/// - PHP `sence != 'image'` 直接 `return false` → Rust 端不提供此分支
276///   (业务层应直接拒绝非 image 场景,或使用其他校验器)
277#[derive(Debug, Clone)]
278pub struct FileValidator {
279    rule: FileValidateRule,
280    messages: FileValidateMessages,
281}
282
283impl Default for FileValidator {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289impl FileValidator {
290    /// 创建校验器(使用默认图片规则和消息,对齐 PHP `Driver::validate` 默认行为)
291    pub fn new() -> Self {
292        Self {
293            rule: FileValidateRule::default_image(),
294            messages: FileValidateMessages::default_image(),
295        }
296    }
297
298    /// 创建校验器(自定义规则和消息)
299    pub fn with(rule: FileValidateRule, messages: FileValidateMessages) -> Self {
300        Self { rule, messages }
301    }
302
303    /// 获取校验规则引用
304    pub fn rule(&self) -> &FileValidateRule {
305        &self.rule
306    }
307
308    /// 获取校验消息引用
309    pub fn messages(&self) -> &FileValidateMessages {
310        &self.messages
311    }
312
313    /// 校验扩展名 — 对齐 PHP `Validate::checkExt` 第 957-964 行
314    ///
315    /// PHP 行为:
316    /// ```php
317    /// protected function checkExt(File $file, $ext): bool {
318    ///     if (is_string($ext)) { $ext = explode(',', $ext); }
319    ///     return in_array(strtolower($file->extension()), $ext);
320    /// }
321    /// ```
322    ///
323    /// **注意**:`$file->extension()` 对 `UploadedFile` 会调用覆写版本(返回 `original_extension`)。
324    pub fn check_ext(file: &UploadedFile, allowed: &[String]) -> bool {
325        let ext = file.extension().to_lowercase();
326        allowed.contains(&ext)
327    }
328
329    /// 校验 MIME — 对齐 PHP `Validate::checkMime` 第 985-992 行
330    ///
331    /// PHP 行为:
332    /// ```php
333    /// protected function checkMime(File $file, $mime): bool {
334    ///     if (is_string($mime)) { $mime = explode(',', $mime); }
335    ///     return in_array(strtolower($file->getMime()), $mime);
336    /// }
337    /// ```
338    ///
339    /// **注意**:`$file->getMime()` 使用服务器端 `finfo_file` 检测(Rust 端使用 `infer` crate)。
340    pub fn check_mime(file: &File, allowed: &[String]) -> Result<bool, FileValidateError> {
341        let mime = file
342            .get_mime()
343            .map_err(|e| FileValidateError::MimeDetect(e.to_string()))?;
344        Ok(allowed.contains(&mime.to_lowercase()))
345    }
346
347    /// 校验大小 — 对齐 PHP `Validate::checkSize` 第 973-976 行
348    ///
349    /// PHP 行为:
350    /// ```php
351    /// protected function checkSize(File $file, $size): bool {
352    ///     return $file->getSize() <= (int) $size;
353    /// }
354    /// ```
355    pub fn check_size(file: &File, max: u64) -> Result<bool, FileValidateError> {
356        let actual = file.path().metadata()?.len();
357        Ok(actual <= max)
358    }
359
360    /// 校验文件 — 对齐 PHP `Driver::validate($name, $fileInfo, 'image')`
361    ///
362    /// 执行顺序:扩展名 → MIME → 大小(对齐 PHP `validate()->check()` 的规则遍历顺序)
363    ///
364    /// ## 返回值
365    ///
366    /// - `Ok(())`:校验通过
367    /// - `Err(FileValidateError::*)`:校验失败,包含错误消息(对齐 PHP `$e->getMessage()`)
368    pub fn validate_image(&self, file: &UploadedFile) -> Result<(), FileValidateError> {
369        // 对齐 PHP checkExt:strtolower($file->extension())
370        if let Some(ref allowed_ext) = self.rule.file_ext {
371            if !Self::check_ext(file, allowed_ext) {
372                return Err(FileValidateError::ExtNotAllowed {
373                    ext: file.extension().to_lowercase(),
374                    allowed: allowed_ext.clone(),
375                    msg: self.messages.file_ext.clone(),
376                });
377            }
378        }
379
380        // 对齐 PHP checkMime:strtolower($file->getMime())
381        if let Some(ref allowed_mime) = self.rule.file_mime {
382            if !Self::check_mime(file.as_file(), allowed_mime)? {
383                return Err(FileValidateError::MimeNotAllowed {
384                    mime: file.as_file().get_mime().unwrap_or_default().to_lowercase(),
385                    allowed: allowed_mime.clone(),
386                    msg: self.messages.file_mime.clone(),
387                });
388            }
389        }
390
391        // 对齐 PHP checkSize:$file->getSize() <= (int) $size
392        if let Some(max_size) = self.rule.file_size {
393            let actual = file.as_file().path().metadata()?.len();
394            if actual > max_size {
395                return Err(FileValidateError::SizeExceeded {
396                    actual,
397                    max: max_size,
398                    msg: self.messages.file_size.clone(),
399                });
400            }
401        }
402
403        Ok(())
404    }
405}
406
407// ============================================================================
408// 文件类型分类
409// ============================================================================
410
411/// 文件类型分类 — 对齐 PHP `app\api\controller\file\Upload.php` 第 62-69 行
412///
413/// PHP 原始逻辑:
414/// ```php
415/// if(in_array($extension,['jpg','png','jpeg','bmp','gif','icon','svg','tif','webp','tiff','avif','pjp'])){
416///     $file_type = 'image';
417/// } else if(in_array($extension,['mp4','m3u8','mp3','wmv','mpg','webm','mov','avi','m4v','mpeg','ogv','asx','ogm'])){
418///     $file_type = 'video';
419/// } else {
420///     $file_type = 'file';
421/// }
422/// ```
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub enum FileType {
425    /// 图片(image) — 12 种扩展名
426    Image,
427    /// 视频(video) — 13 种扩展名
428    Video,
429    /// 其他文件(file)
430    File,
431}
432
433impl FileType {
434    /// 转为字符串(对齐 PHP `$file_type` 字符串值)
435    pub fn as_str(self) -> &'static str {
436        match self {
437            FileType::Image => "image",
438            FileType::Video => "video",
439            FileType::File => "file",
440        }
441    }
442}
443
444/// 图片扩展名白名单 — 对齐 PHP `Upload.php` 第 63 行
445const IMAGE_EXTS: &[&str] = &[
446    "jpg", "png", "jpeg", "bmp", "gif", "icon", "svg", "tif", "webp", "tiff", "avif", "pjp",
447];
448
449/// 视频扩展名白名单 — 对齐 PHP `Upload.php` 第 65 行
450const VIDEO_EXTS: &[&str] = &[
451    "mp4", "m3u8", "mp3", "wmv", "mpg", "webm", "mov", "avi", "m4v", "mpeg", "ogv", "asx", "ogm",
452];
453
454/// 检测文件类型 — 对齐 PHP `in_array($extension, [...])` 分类逻辑
455///
456/// ## 参数
457///
458/// - `ext`:文件扩展名(不区分大小写,内部 lowercase 后匹配)
459///
460/// ## 返回值
461///
462/// - [`FileType::Image`]:扩展名在 `IMAGE_EXTS` 中
463/// - [`FileType::Video`]:扩展名在 `VIDEO_EXTS` 中
464/// - [`FileType::File`]:其他
465pub fn detect_file_type(ext: &str) -> FileType {
466    let ext_lower = ext.to_lowercase();
467    if IMAGE_EXTS.contains(&ext_lower.as_str()) {
468        FileType::Image
469    } else if VIDEO_EXTS.contains(&ext_lower.as_str()) {
470        FileType::Video
471    } else {
472        FileType::File
473    }
474}
475
476// ============================================================================
477// 辅助函数
478// ============================================================================
479
480/// 解析扩展名列表 — 对齐 PHP `explode(',', $ext)` + `strtolower`
481///
482/// PHP 行为(`Validate::checkExt` 第 959-961 行):
483/// ```php
484/// if (is_string($ext)) {
485///     $ext = explode(',', $ext);
486/// }
487/// ```
488///
489/// Rust 端额外处理:
490/// - `trim()` 去除空格(PHP `explode` 不 trim)
491/// - 过滤空字符串(PHP `explode` 会保留空元素,但 `in_array` 不会匹配空扩展名)
492pub fn parse_ext_list(s: &str) -> Vec<String> {
493    s.split(',')
494        .map(|p| p.trim().to_lowercase())
495        .filter(|p| !p.is_empty())
496        .collect()
497}
498
499/// 解析 MIME 列表 — 对齐 PHP `explode(',', $mime)` + `strtolower`
500///
501/// PHP 行为(`Validate::checkMime` 第 987-989 行):
502/// ```php
503/// if (is_string($mime)) {
504///     $mime = explode(',', $mime);
505/// }
506/// ```
507pub fn parse_mime_list(s: &str) -> Vec<String> {
508    s.split(',')
509        .map(|p| p.trim().to_lowercase())
510        .filter(|p| !p.is_empty())
511        .collect()
512}
513
514// ============================================================================
515// 单元测试
516// ============================================================================
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use crate::upload::UploadedFile;
522    use std::io::Write;
523
524    // ---- 辅助函数 ----
525
526    /// 创建临时文件并写入内容
527    fn create_temp_file(content: &[u8], suffix: &str) -> tempfile::NamedTempFile {
528        let mut temp = tempfile::Builder::new()
529            .suffix(suffix)
530            .tempfile()
531            .expect("创建临时文件失败");
532        temp.write_all(content).expect("写入临时文件失败");
533        temp.flush().expect("flush 失败");
534        temp
535    }
536
537    // ========================================================================
538    // 组 1:FileValidateRule 基础
539    // ========================================================================
540
541    #[test]
542    fn test_rule_new() {
543        let rule = FileValidateRule::new();
544        assert!(rule.file_size.is_none());
545        assert!(rule.file_ext.is_none());
546        assert!(rule.file_mime.is_none());
547    }
548
549    #[test]
550    fn test_rule_with_size() {
551        let rule = FileValidateRule::new().with_size(1024);
552        assert_eq!(rule.file_size, Some(1024));
553    }
554
555    #[test]
556    fn test_rule_with_ext() {
557        let rule = FileValidateRule::new().with_ext("jpg,png,GIF");
558        assert_eq!(
559            rule.file_ext,
560            Some(vec![
561                "jpg".to_string(),
562                "png".to_string(),
563                "gif".to_string(),
564            ])
565        );
566    }
567
568    #[test]
569    fn test_rule_with_mime() {
570        let rule = FileValidateRule::new().with_mime("image/jpeg,image/png");
571        assert_eq!(
572            rule.file_mime,
573            Some(vec!["image/jpeg".to_string(), "image/png".to_string(),])
574        );
575    }
576
577    #[test]
578    fn test_rule_default_image() {
579        let rule = FileValidateRule::default_image();
580        // 对齐 PHP Driver.php 第 30 行:fileSize = 20971520
581        assert_eq!(rule.file_size, Some(20 * 1024 * 1024));
582        // 对齐 PHP Driver.php 第 31 行:fileExt = 'jpg,jpeg,png,gif,bmp'
583        assert_eq!(
584            rule.file_ext,
585            Some(vec![
586                "jpg".to_string(),
587                "jpeg".to_string(),
588                "png".to_string(),
589                "gif".to_string(),
590                "bmp".to_string(),
591            ])
592        );
593        // 对齐 PHP Driver.php 第 32 行:fileMime = 'image/jpeg,image/png,image/gif,image/bmp'
594        assert_eq!(
595            rule.file_mime,
596            Some(vec![
597                "image/jpeg".to_string(),
598                "image/png".to_string(),
599                "image/gif".to_string(),
600                "image/bmp".to_string(),
601            ])
602        );
603    }
604
605    #[test]
606    fn test_rule_with_ext_vec() {
607        let rule = FileValidateRule::new().with_ext_vec(vec!["JPG".to_string(), "PNG".to_string()]);
608        assert_eq!(
609            rule.file_ext,
610            Some(vec!["jpg".to_string(), "png".to_string(),])
611        );
612    }
613
614    #[test]
615    fn test_rule_with_mime_vec() {
616        let rule = FileValidateRule::new()
617            .with_mime_vec(vec!["IMAGE/JPEG".to_string(), "IMAGE/PNG".to_string()]);
618        assert_eq!(
619            rule.file_mime,
620            Some(vec!["image/jpeg".to_string(), "image/png".to_string(),])
621        );
622    }
623
624    // ========================================================================
625    // 组 2:FileValidateMessages
626    // ========================================================================
627
628    #[test]
629    fn test_messages_default() {
630        let msgs = FileValidateMessages::default();
631        // 对齐 PHP zh-cn.php 第 90-92 行
632        assert_eq!(msgs.file_size, "上传文件大小不符!");
633        assert_eq!(msgs.file_ext, "上传文件后缀不允许");
634        assert_eq!(msgs.file_mime, "上传文件MIME类型不允许!");
635    }
636
637    #[test]
638    fn test_messages_default_image() {
639        let msgs = FileValidateMessages::default_image();
640        // 对齐 PHP Driver.php 第 35-37 行
641        assert_eq!(msgs.file_size, "最大可上传2M图片");
642        assert_eq!(msgs.file_ext, "只能上传jpg,jpeg,png,gif,bmp格式图片");
643        assert_eq!(msgs.file_mime, "只能上传jpg,jpeg,png,gif,bmp格式图片");
644    }
645
646    #[test]
647    fn test_messages_custom() {
648        let msgs = FileValidateMessages {
649            file_size: "文件太大".to_string(),
650            file_ext: "格式不对".to_string(),
651            file_mime: "MIME不对".to_string(),
652        };
653        assert_eq!(msgs.file_size, "文件太大");
654        assert_eq!(msgs.file_ext, "格式不对");
655        assert_eq!(msgs.file_mime, "MIME不对");
656    }
657
658    // ========================================================================
659    // 组 3:FileValidator 基础
660    // ========================================================================
661
662    #[test]
663    fn test_validator_new() {
664        let v = FileValidator::new();
665        assert_eq!(v.rule().file_size, Some(20 * 1024 * 1024));
666        assert_eq!(v.messages().file_size, "最大可上传2M图片");
667    }
668
669    #[test]
670    fn test_validator_with_custom() {
671        let rule = FileValidateRule::new().with_ext("pdf,doc");
672        let msgs = FileValidateMessages::default();
673        let v = FileValidator::with(rule, msgs);
674        assert_eq!(
675            v.rule().file_ext,
676            Some(vec!["pdf".to_string(), "doc".to_string()])
677        );
678        assert_eq!(v.messages().file_ext, "上传文件后缀不允许");
679    }
680
681    // ========================================================================
682    // 组 4:FileValidator::check_ext
683    // ========================================================================
684
685    #[test]
686    fn test_check_ext_pass() {
687        let temp = create_temp_file(b"hello", ".jpg");
688        let file = UploadedFile::new(temp.path(), "photo.JPG", None, Some(0), true).unwrap();
689        let allowed = parse_ext_list("jpg,jpeg,png,gif,bmp");
690        // 对齐 PHP checkExt:strtolower($file->extension()) in_array
691        assert!(FileValidator::check_ext(&file, &allowed));
692    }
693
694    #[test]
695    fn test_check_ext_fail() {
696        let temp = create_temp_file(b"hello", ".txt");
697        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
698        let allowed = parse_ext_list("jpg,jpeg,png,gif,bmp");
699        assert!(!FileValidator::check_ext(&file, &allowed));
700    }
701
702    #[test]
703    fn test_check_ext_case_insensitive() {
704        // 对齐 PHP strtolower:扩展名大写也能匹配小写白名单
705        let temp = create_temp_file(b"hello", ".jpg");
706        let file = UploadedFile::new(temp.path(), "photo.JPEG", None, Some(0), true).unwrap();
707        let allowed = parse_ext_list("jpg,jpeg");
708        assert!(FileValidator::check_ext(&file, &allowed));
709    }
710
711    // ========================================================================
712    // 组 5:FileValidator::check_size
713    // ========================================================================
714
715    #[test]
716    fn test_check_size_pass() {
717        let temp = create_temp_file(b"hello", ".txt");
718        let file = File::new(temp.path(), false).unwrap();
719        // 5 bytes <= 100
720        assert!(FileValidator::check_size(&file, 100).unwrap());
721    }
722
723    #[test]
724    fn test_check_size_equal() {
725        // 对齐 PHP <= 语义:等于也算通过
726        let temp = create_temp_file(b"hello", ".txt");
727        let file = File::new(temp.path(), false).unwrap();
728        // 5 bytes <= 5
729        assert!(FileValidator::check_size(&file, 5).unwrap());
730    }
731
732    #[test]
733    fn test_check_size_fail() {
734        let temp = create_temp_file(b"hello world", ".txt");
735        let file = File::new(temp.path(), false).unwrap();
736        // 11 bytes > 5
737        assert!(!FileValidator::check_size(&file, 5).unwrap());
738    }
739
740    // ========================================================================
741    // 组 6:FileValidator::validate_image
742    // ========================================================================
743
744    #[test]
745    fn test_validate_image_ext_pass() {
746        let temp = create_temp_file(b"\x89PNG\r\n\x1a\n", ".png");
747        let file = UploadedFile::new(temp.path(), "photo.png", None, Some(0), true).unwrap();
748        let v = FileValidator::new();
749        // 扩展名 png 在白名单中
750        let result = v.validate_image(&file);
751        // MIME 可能不匹配(因为是假 PNG),但扩展名应该通过
752        // 这里只验证扩展名通过(可能因 MIME 失败,但不应该是 ExtNotAllowed)
753        match result {
754            Ok(()) => {}
755            Err(FileValidateError::ExtNotAllowed { .. }) => panic!("扩展名应通过"),
756            Err(_) => {}
757        }
758    }
759
760    #[test]
761    fn test_validate_image_ext_fail() {
762        let temp = create_temp_file(b"hello", ".txt");
763        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
764        let v = FileValidator::new();
765        let result = v.validate_image(&file);
766        // 对齐 PHP:扩展名 txt 不在白名单 → ExtNotAllowed
767        assert!(matches!(
768            result,
769            Err(FileValidateError::ExtNotAllowed { .. })
770        ));
771    }
772
773    #[test]
774    fn test_validate_image_size_fail() {
775        // 创建一个扩展名和 MIME 都通过但大小超限的文件
776        // 使用自定义规则:只校验大小
777        let temp = create_temp_file(b"hello world, this is a long file", ".jpg");
778        let file = UploadedFile::new(temp.path(), "photo.jpg", None, Some(0), true).unwrap();
779        let rule = FileValidateRule::new().with_size(5); // 只校验大小 <= 5
780        let v = FileValidator::with(rule, FileValidateMessages::default());
781        let result = v.validate_image(&file);
782        assert!(matches!(
783            result,
784            Err(FileValidateError::SizeExceeded { .. })
785        ));
786    }
787
788    #[test]
789    fn test_validate_image_all_pass() {
790        // 创建一个真实的 PNG 文件
791        let png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01";
792        let temp = create_temp_file(png_header, ".png");
793        let file = UploadedFile::new(temp.path(), "photo.png", None, Some(0), true).unwrap();
794        let rule = FileValidateRule::new()
795            .with_ext("png")
796            .with_mime("image/png")
797            .with_size(1024);
798        let v = FileValidator::with(rule, FileValidateMessages::default());
799        let result = v.validate_image(&file);
800        assert!(result.is_ok(), "校验应通过: {:?}", result);
801    }
802
803    #[test]
804    fn test_validate_image_no_rule_passes() {
805        // 空规则 → 任何文件都通过
806        let temp = create_temp_file(b"hello", ".txt");
807        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
808        let v = FileValidator::with(FileValidateRule::new(), FileValidateMessages::default());
809        let result = v.validate_image(&file);
810        assert!(result.is_ok());
811    }
812
813    #[test]
814    fn test_validate_image_error_messages() {
815        // 对齐 PHP Driver.php 第 34-38 行自定义消息
816        let temp = create_temp_file(b"hello", ".txt");
817        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
818        let v = FileValidator::new(); // 默认图片规则和消息
819        let result = v.validate_image(&file);
820        match result {
821            Err(FileValidateError::ExtNotAllowed { msg, .. }) => {
822                assert_eq!(msg, "只能上传jpg,jpeg,png,gif,bmp格式图片");
823            }
824            _ => panic!("应返回 ExtNotAllowed"),
825        }
826    }
827
828    // ========================================================================
829    // 组 7:detect_file_type
830    // ========================================================================
831
832    #[test]
833    fn test_detect_file_type_image() {
834        // 对齐 PHP Upload.php 第 63 行:12 种图片扩展名
835        assert_eq!(detect_file_type("jpg"), FileType::Image);
836        assert_eq!(detect_file_type("png"), FileType::Image);
837        assert_eq!(detect_file_type("jpeg"), FileType::Image);
838        assert_eq!(detect_file_type("bmp"), FileType::Image);
839        assert_eq!(detect_file_type("gif"), FileType::Image);
840        assert_eq!(detect_file_type("icon"), FileType::Image);
841        assert_eq!(detect_file_type("svg"), FileType::Image);
842        assert_eq!(detect_file_type("tif"), FileType::Image);
843        assert_eq!(detect_file_type("webp"), FileType::Image);
844        assert_eq!(detect_file_type("tiff"), FileType::Image);
845        assert_eq!(detect_file_type("avif"), FileType::Image);
846        assert_eq!(detect_file_type("pjp"), FileType::Image);
847    }
848
849    #[test]
850    fn test_detect_file_type_video() {
851        // 对齐 PHP Upload.php 第 65 行:13 种视频扩展名
852        assert_eq!(detect_file_type("mp4"), FileType::Video);
853        assert_eq!(detect_file_type("m3u8"), FileType::Video);
854        assert_eq!(detect_file_type("mp3"), FileType::Video);
855        assert_eq!(detect_file_type("wmv"), FileType::Video);
856        assert_eq!(detect_file_type("mpg"), FileType::Video);
857        assert_eq!(detect_file_type("webm"), FileType::Video);
858        assert_eq!(detect_file_type("mov"), FileType::Video);
859        assert_eq!(detect_file_type("avi"), FileType::Video);
860        assert_eq!(detect_file_type("m4v"), FileType::Video);
861        assert_eq!(detect_file_type("mpeg"), FileType::Video);
862        assert_eq!(detect_file_type("ogv"), FileType::Video);
863        assert_eq!(detect_file_type("asx"), FileType::Video);
864        assert_eq!(detect_file_type("ogm"), FileType::Video);
865    }
866
867    #[test]
868    fn test_detect_file_type_file() {
869        // 其他扩展名 → File
870        assert_eq!(detect_file_type("pdf"), FileType::File);
871        assert_eq!(detect_file_type("doc"), FileType::File);
872        assert_eq!(detect_file_type("xls"), FileType::File);
873        assert_eq!(detect_file_type("zip"), FileType::File);
874        assert_eq!(detect_file_type("exe"), FileType::File);
875        assert_eq!(detect_file_type("php"), FileType::File);
876    }
877
878    #[test]
879    fn test_detect_file_type_case_insensitive() {
880        // 对齐 PHP in_array 默认区分大小写,但业务代码通常先 lowercase
881        // Rust 端内部 lowercase,对齐 PHP 业务实践
882        assert_eq!(detect_file_type("JPG"), FileType::Image);
883        assert_eq!(detect_file_type("MP4"), FileType::Video);
884        assert_eq!(detect_file_type("PDF"), FileType::File);
885    }
886
887    #[test]
888    fn test_file_type_as_str() {
889        assert_eq!(FileType::Image.as_str(), "image");
890        assert_eq!(FileType::Video.as_str(), "video");
891        assert_eq!(FileType::File.as_str(), "file");
892    }
893
894    // ========================================================================
895    // 组 8:辅助函数
896    // ========================================================================
897
898    #[test]
899    fn test_parse_ext_list_basic() {
900        // 对齐 PHP explode(',', $ext)
901        let list = parse_ext_list("jpg,jpeg,png,gif,bmp");
902        assert_eq!(list, vec!["jpg", "jpeg", "png", "gif", "bmp"]);
903    }
904
905    #[test]
906    fn test_parse_ext_list_lowercase() {
907        // 对齐 PHP strtolower
908        let list = parse_ext_list("JPG,JPEG,PNG");
909        assert_eq!(list, vec!["jpg", "jpeg", "png"]);
910    }
911
912    #[test]
913    fn test_parse_ext_list_trim() {
914        // PHP explode 不 trim,但 Rust 端额外 trim(更宽松)
915        let list = parse_ext_list("jpg, jpeg , png");
916        assert_eq!(list, vec!["jpg", "jpeg", "png"]);
917    }
918
919    #[test]
920    fn test_parse_ext_list_empty() {
921        let list = parse_ext_list("");
922        assert!(list.is_empty());
923    }
924
925    #[test]
926    fn test_parse_mime_list_basic() {
927        let list = parse_mime_list("image/jpeg,image/png,image/gif,image/bmp");
928        assert_eq!(
929            list,
930            vec!["image/jpeg", "image/png", "image/gif", "image/bmp"]
931        );
932    }
933
934    #[test]
935    fn test_parse_mime_list_lowercase() {
936        let list = parse_mime_list("IMAGE/JPEG,IMAGE/PNG");
937        assert_eq!(list, vec!["image/jpeg", "image/png"]);
938    }
939
940    // ========================================================================
941    // 组 9:PHP 行为对齐 R5
942    // ========================================================================
943
944    /// R5-13:默认图片规则对齐 PHP Driver.php 第 30-32 行
945    #[test]
946    fn test_php_behavior_default_image_rule() {
947        let rule = FileValidateRule::default_image();
948        // PHP Driver.php 第 30 行:fileSize = 20971520
949        assert_eq!(rule.file_size, Some(20971520));
950        // PHP Driver.php 第 31 行:fileExt = 'jpg,jpeg,png,gif,bmp'
951        assert_eq!(
952            rule.file_ext,
953            Some(
954                ["jpg", "jpeg", "png", "gif", "bmp"]
955                    .iter()
956                    .map(|&s| s.to_string())
957                    .collect::<Vec<_>>()
958            )
959        );
960        // PHP Driver.php 第 32 行:fileMime = 'image/jpeg,image/png,image/gif,image/bmp'
961        assert_eq!(
962            rule.file_mime,
963            Some(
964                ["image/jpeg", "image/png", "image/gif", "image/bmp"]
965                    .iter()
966                    .map(|&s| s.to_string())
967                    .collect::<Vec<_>>()
968            )
969        );
970    }
971
972    /// R5-14:默认图片消息对齐 PHP Driver.php 第 34-38 行
973    #[test]
974    fn test_php_behavior_default_image_messages() {
975        let msgs = FileValidateMessages::default_image();
976        // PHP Driver.php 第 35 行
977        assert_eq!(msgs.file_size, "最大可上传2M图片");
978        // PHP Driver.php 第 36 行
979        assert_eq!(msgs.file_ext, "只能上传jpg,jpeg,png,gif,bmp格式图片");
980        // PHP Driver.php 第 37 行
981        assert_eq!(msgs.file_mime, "只能上传jpg,jpeg,png,gif,bmp格式图片");
982    }
983
984    /// R5-9:checkExt 使用 strtolower + in_array
985    #[test]
986    fn test_php_behavior_check_ext_lowercase() {
987        let temp = create_temp_file(b"hello", ".jpg");
988        // 原始扩展名大写 → strtolower 后匹配
989        let file = UploadedFile::new(temp.path(), "PHOTO.JPG", None, Some(0), true).unwrap();
990        let allowed = parse_ext_list("jpg,jpeg,png,gif,bmp");
991        // 对齐 PHP strtolower($file->extension()) in_array
992        assert!(FileValidator::check_ext(&file, &allowed));
993    }
994
995    /// R5-10:checkMime 使用 strtolower + in_array
996    #[test]
997    fn test_php_behavior_check_mime_lowercase() {
998        // 创建真实 PNG 文件
999        let png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01";
1000        let temp = create_temp_file(png_header, ".png");
1001        let file = File::new(temp.path(), false).unwrap();
1002        // MIME 白名单小写
1003        let allowed = parse_mime_list("image/png,image/jpeg");
1004        // 对齐 PHP strtolower($file->getMime()) in_array
1005        assert!(FileValidator::check_mime(&file, &allowed).unwrap());
1006    }
1007
1008    /// R5-11:checkSize 使用 <= 语义
1009    #[test]
1010    fn test_php_behavior_check_size_leq() {
1011        // 对齐 PHP $file->getSize() <= (int) $size
1012        let temp = create_temp_file(b"hello", ".txt"); // 5 bytes
1013        let file = File::new(temp.path(), false).unwrap();
1014        // 等于 → 通过
1015        assert!(FileValidator::check_size(&file, 5).unwrap());
1016        // 小于 → 通过
1017        assert!(FileValidator::check_size(&file, 10).unwrap());
1018        // 大于 → 失败
1019        assert!(!FileValidator::check_size(&file, 4).unwrap());
1020    }
1021
1022    /// R5-15:文件类型分类 image 12 种 / video 13 种 / file 其他
1023    #[test]
1024    fn test_php_behavior_file_type_classification() {
1025        // 图片 12 种(对齐 PHP Upload.php 第 63 行)
1026        let image_count = [
1027            "jpg", "png", "jpeg", "bmp", "gif", "icon", "svg", "tif", "webp", "tiff", "avif", "pjp",
1028        ]
1029        .iter()
1030        .filter(|&&e| detect_file_type(e) == FileType::Image)
1031        .count();
1032        assert_eq!(image_count, 12);
1033
1034        // 视频 13 种(对齐 PHP Upload.php 第 65 行)
1035        let video_count = [
1036            "mp4", "m3u8", "mp3", "wmv", "mpg", "webm", "mov", "avi", "m4v", "mpeg", "ogv", "asx",
1037            "ogm",
1038        ]
1039        .iter()
1040        .filter(|&&e| detect_file_type(e) == FileType::Video)
1041        .count();
1042        assert_eq!(video_count, 13);
1043
1044        // 其他 → File
1045        assert_eq!(detect_file_type("pdf"), FileType::File);
1046        assert_eq!(detect_file_type("xyz"), FileType::File);
1047        assert_eq!(detect_file_type(""), FileType::File);
1048    }
1049
1050    /// R5-12:Driver::validate 仅 sence == 'image' 执行校验
1051    /// Rust 端语义:FileValidator::validate_image 仅执行图片校验
1052    #[test]
1053    fn test_php_behavior_validate_image_only() {
1054        // 验证 FileValidator 只有 validate_image 方法(对齐 PHP sence == 'image' 分支)
1055        // 非 image 场景由业务层处理,对齐 PHP sence != 'image' return false
1056        let temp = create_temp_file(b"hello", ".txt");
1057        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
1058        let v = FileValidator::new();
1059        // txt 扩展名不在图片白名单 → 失败
1060        let result = v.validate_image(&file);
1061        assert!(matches!(
1062            result,
1063            Err(FileValidateError::ExtNotAllowed { .. })
1064        ));
1065    }
1066
1067    /// 验证校验顺序:扩展名 → MIME → 大小
1068    #[test]
1069    fn test_validate_order_ext_first() {
1070        // 扩展名失败时不应检查 MIME/大小
1071        let temp = create_temp_file(b"hello", ".txt");
1072        let file = UploadedFile::new(temp.path(), "doc.txt", None, Some(0), true).unwrap();
1073        let rule = FileValidateRule::new()
1074            .with_ext("jpg")
1075            .with_mime("image/jpeg")
1076            .with_size(1); // 大小也会失败,但扩展名先失败
1077        let v = FileValidator::with(rule, FileValidateMessages::default());
1078        let result = v.validate_image(&file);
1079        // 应返回 ExtNotAllowed(而不是 SizeExceeded)
1080        assert!(matches!(
1081            result,
1082            Err(FileValidateError::ExtNotAllowed { .. })
1083        ));
1084    }
1085
1086    /// 验证校验顺序:MIME 先于大小
1087    #[test]
1088    fn test_validate_order_mime_before_size() {
1089        // 扩展名通过,MIME 失败时不应检查大小
1090        // 使用 PNG 头内容 + .jpg 扩展名:infer 检测为 image/png,不匹配 image/jpeg 白名单
1091        let png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01";
1092        let temp = create_temp_file(png_header, ".jpg");
1093        let file = UploadedFile::new(temp.path(), "photo.jpg", None, Some(0), true).unwrap();
1094        let rule = FileValidateRule::new()
1095            .with_ext("jpg")
1096            .with_mime("image/jpeg") // 实际 MIME 是 image/png → 失败
1097            .with_size(1); // 大小也会失败,但 MIME 先失败
1098        let v = FileValidator::with(rule, FileValidateMessages::default());
1099        let result = v.validate_image(&file);
1100        // 应返回 MimeNotAllowed(而不是 SizeExceeded)
1101        assert!(matches!(
1102            result,
1103            Err(FileValidateError::MimeNotAllowed { .. })
1104        ));
1105    }
1106}