Skip to main content

sz_rust_infra_facade/
upload.rs

1//! 文件上传模块 — 对齐 PHP `think\File` + `think\file\UploadedFile`
2//!
3//! 本模块实现文件上传机制,对齐 PHP `think\File` 类
4//! 的 hash/move/hashName/extension/getMime 等核心方法,以及 `think\file\UploadedFile`
5//! 的 isValid/move/getOriginalName/getOriginalExtension 等方法。
6//!
7//! ## PHP 对齐
8//!
9//! ### 核心类映射
10//!
11//! | PHP 类 | Rust 结构 | 说明 |
12//! |---------|-----------|------|
13//! | `think\File`(extends `SplFileInfo`) | [`File`] | 文件类 |
14//! | `think\file\UploadedFile`(extends `File`) | [`UploadedFile`] | 上传文件类 |
15//! | `think\exception\FileException` | [`UploadError`] | 文件异常 |
16//!
17//! ### 核心方法映射
18//!
19//! | PHP 方法 | Rust 方法 | 说明 |
20//! |---------|-----------|------|
21//! | `File::hash($type)` | [`File::hash`] | 获取文件哈希 |
22//! | `File::md5()` | [`File::md5`] | 获取文件 MD5 |
23//! | `File::sha1()` | [`File::sha1`] | 获取文件 SHA1 |
24//! | `File::getMime()` | [`File::get_mime`] | 获取文件 MIME |
25//! | `File::move($dir, $name)` | [`File::move_to`] | 移动文件 |
26//! | `File::extension()` | [`File::extension`] | 文件扩展名 |
27//! | `File::setExtension($ext)` | [`File::set_extension`] | 设置扩展名 |
28//! | `File::hashName($rule)` | [`File::hash_name`] | 生成哈希文件名 |
29//! | `UploadedFile::isValid()` | [`UploadedFile::is_valid`] | 验证上传文件 |
30//! | `UploadedFile::move($dir, $name)` | [`UploadedFile::move_to`] | 移动上传文件 |
31//! | `UploadedFile::getOriginalMime()` | [`UploadedFile::original_mime`] | 原始 MIME |
32//! | `UploadedFile::getOriginalName()` | [`UploadedFile::original_name`] | 原始文件名 |
33//! | `UploadedFile::getOriginalExtension()` | [`UploadedFile::original_extension`] | 原始扩展名 |
34//! | `UploadedFile::extension()` | [`UploadedFile::extension`] | 覆写父类扩展名 |
35//!
36//! ## PHP 行为对齐(R5 硬约束)
37//!
38//! - **R5-1**:`hashName` 默认规则 = `date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true) . pathname)`
39//!   (对齐 PHP 第 195 行)
40//! - **R5-2**:`hashName` hash 算法规则 = `substr(hash, 0, 2) . DIRECTORY_SEPARATOR . substr(hash, 2)`
41//!   (对齐 PHP 第 187-190 行)
42//! - **R5-3**:`UploadedFile::isValid` = `error == UPLOAD_ERR_OK && is_uploaded_file(pathname)`
43//!   (对齐 PHP 第 36-41 行)
44//! - **R5-4**:`UploadedFile::move` 使用 `move_uploaded_file`(test 模式使用 `rename`)
45//!   (对齐 PHP 第 50-75 行)
46//! - **R5-5**:`UploadedFile::getErrorMessage` 错误码映射
47//!   (对齐 PHP 第 82-106 行)
48//! - **R5-6**:`UploadedFile::extension()` 覆写父类,返回原始扩展名
49//!   (对齐 PHP 第 139-142 行)
50//! - **R5-7**:`File::getMime` 使用 `finfo_file(FILEINFO_MIME_TYPE)` 对齐 Rust `infer` crate
51//!   (对齐 PHP 第 88-93 行)
52//! - **R5-8**:`File::move` 创建目录 `mkdir(dir, 0777, true)` + `chmod(target, 0666 & ~umask())`
53//!   (对齐 PHP 第 102-118 行)
54//!
55//! ## PHP 源码参考
56//!
57//! - `e:\vue\test\鲜视达\server\vendor\topthink\framework\src\think\File.php`
58//!   - 第 22-46 行:类声明 + 构造方法
59//!   - 第 54-61 行:`hash($type)` 方法
60//!   - 第 88-93 行:`getMime()` 方法
61//!   - 第 102-118 行:`move($directory, $name)` 方法
62//!   - 第 126-139 行:`getTargetFile($directory, $name)` 方法
63//!   - 第 146-153 行:`getName($name)` 方法
64//!   - 第 159-162 行:`extension()` 方法
65//!   - 第 169-172 行:`setExtension($extension)` 方法
66//!   - 第 180-203 行:`hashName($rule)` 方法
67//! - `e:\vue\test\鲜视达\server\vendor\topthink\framework\src\think\file\UploadedFile.php`
68//!   - 第 18-34 行:类声明 + 构造方法
69//!   - 第 36-41 行:`isValid()` 方法
70//!   - 第 50-75 行:`move($directory, $name)` 方法
71//!   - 第 82-106 行:`getErrorMessage()` 方法
72//!   - 第 112-142 行:`getOriginalMime/Name/Extension` + `extension()` 方法
73
74use std::collections::HashMap;
75use std::fs;
76use std::io::Read;
77use std::path::{Path, PathBuf};
78
79use chrono::Local;
80use md5::{Digest, Md5};
81
82// ============================================================================
83// 子模块
84// ============================================================================
85
86pub mod image;
87pub mod storage;
88pub mod validate;
89
90// ============================================================================
91// 错误类型
92// ============================================================================
93
94/// 上传错误 — 对齐 PHP `think\exception\FileException`
95#[derive(Debug, thiserror::Error)]
96pub enum UploadError {
97    /// 文件不存在(对齐 PHP 第 42 行:`The file "%s" does not exist`)
98    #[error("The file \"{0}\" does not exist")]
99    FileNotFound(String),
100
101    /// 文件移动失败(对齐 PHP 第 112 行:`Could not move the file "%s" to "%s" (%s)`)
102    #[error("Could not move the file \"{from}\" to \"{to}\" ({error})")]
103    MoveFailed {
104        /// 源文件路径
105        from: String,
106        /// 目标文件路径
107        to: String,
108        /// 错误信息
109        error: String,
110    },
111
112    /// 目录创建失败(对齐 PHP 第 130 行:`Unable to create the "%s" directory`)
113    #[error("Unable to create the \"{0}\" directory")]
114    DirectoryCreateFailed(String),
115
116    /// 目录不可写(对齐 PHP 第 133 行:`Unable to write in the "%s" directory`)
117    #[error("Unable to write in the \"{0}\" directory")]
118    DirectoryNotWritable(String),
119
120    /// 上传失败(对齐 PHP `UploadedFile::getErrorMessage()`)
121    #[error("{0}")]
122    UploadFailed(String),
123
124    /// IO 错误
125    #[error(transparent)]
126    Io(#[from] std::io::Error),
127
128    /// 临时文件持久化错误(对齐 `tempfile::PersistError`)
129    #[error(transparent)]
130    Persist(#[from] tempfile::PersistError),
131
132    /// 文件名非法(路径遍历攻击防护)
133    #[error("Invalid file name \"{0}\" — potential path traversal attack")]
134    InvalidFileName(String),
135}
136
137// ============================================================================
138// PHP UPLOAD_ERR_* 常量
139// ============================================================================
140
141/// PHP `UPLOAD_ERR_*` 常量 — 对齐 PHP 上传错误码
142///
143/// 对齐 PHP `UploadedFile.php` 第 82-106 行 `getErrorMessage()` 方法。
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145#[repr(i32)]
146pub enum UploadErrCode {
147    /// `UPLOAD_ERR_OK` = 0
148    Ok = 0,
149    /// `UPLOAD_ERR_INI_SIZE` = 1
150    IniSize = 1,
151    /// `UPLOAD_ERR_FORM_SIZE` = 2
152    FormSize = 2,
153    /// `UPLOAD_ERR_PARTIAL` = 3
154    Partial = 3,
155    /// `UPLOAD_ERR_NO_FILE` = 4
156    NoFile = 4,
157    /// `UPLOAD_ERR_NO_TMP_DIR` = 6
158    NoTmpDir = 6,
159    /// `UPLOAD_ERR_CANT_WRITE` = 7
160    CantWrite = 7,
161}
162
163impl UploadErrCode {
164    /// 对齐 PHP `UploadedFile::getErrorMessage` 第 82-106 行
165    ///
166    /// PHP 行为:
167    /// - `1` / `2` → `upload File size exceeds the maximum value`
168    /// - `3` → `only the portion of file is uploaded`
169    /// - `4` → `no file to uploaded`
170    /// - `6` → `upload temp dir not found`
171    /// - `7` → `file write error`
172    /// - `default`(含 `0`)→ `unknown upload error`
173    pub fn error_message(self) -> &'static str {
174        match self {
175            UploadErrCode::IniSize | UploadErrCode::FormSize => {
176                "upload File size exceeds the maximum value"
177            }
178            UploadErrCode::Partial => "only the portion of file is uploaded",
179            UploadErrCode::NoFile => "no file to uploaded",
180            UploadErrCode::NoTmpDir => "upload temp dir not found",
181            UploadErrCode::CantWrite => "file write error",
182            UploadErrCode::Ok => "unknown upload error",
183        }
184    }
185
186    /// 从 i32 转换(对齐 PHP `$error ?: UPLOAD_ERR_OK`)
187    pub fn from_i32(code: i32) -> Self {
188        match code {
189            0 => UploadErrCode::Ok,
190            1 => UploadErrCode::IniSize,
191            2 => UploadErrCode::FormSize,
192            3 => UploadErrCode::Partial,
193            4 => UploadErrCode::NoFile,
194            6 => UploadErrCode::NoTmpDir,
195            7 => UploadErrCode::CantWrite,
196            _ => UploadErrCode::Ok,
197        }
198    }
199}
200
201// ============================================================================
202// 哈希算法
203// ============================================================================
204
205/// 哈希算法 — 对齐 PHP `hash_algos()` 子集
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum HashAlgo {
208    /// MD5
209    Md5,
210    /// SHA1
211    Sha1,
212    /// SHA256
213    Sha256,
214    /// SHA512
215    Sha512,
216}
217
218impl HashAlgo {
219    /// 算法名称(对齐 PHP `hash_algos()` 返回的字符串)
220    pub fn as_str(self) -> &'static str {
221        match self {
222            HashAlgo::Md5 => "md5",
223            HashAlgo::Sha1 => "sha1",
224            HashAlgo::Sha256 => "sha256",
225            HashAlgo::Sha512 => "sha512",
226        }
227    }
228
229    /// 从字符串解析(对齐 PHP `in_array($rule, hash_algos())`)
230    ///
231    /// 注意:不实现 `std::str::FromStr`,因为该 trait 要求返回 `Result` 而非 `Option`,
232    /// 而 PHP `in_array` 语义是布尔判断,使用 `Option` 更贴合。
233    pub fn parse_algo(s: &str) -> Option<Self> {
234        match s {
235            "md5" => Some(HashAlgo::Md5),
236            "sha1" => Some(HashAlgo::Sha1),
237            "sha256" => Some(HashAlgo::Sha256),
238            "sha512" => Some(HashAlgo::Sha512),
239            _ => None,
240        }
241    }
242}
243
244// ============================================================================
245// hashName 规则
246// ============================================================================
247
248/// `hashName` 规则 — 对齐 PHP `File::hashName($rule)` 第 180-203 行
249///
250/// PHP `$rule` 支持 3 种类型:
251/// 1. `Closure` → `call_user_func_array($rule, [$this])`
252/// 2. `string` 在 `hash_algos()` 中 → `substr(hash, 0, 2) . '/' . substr(hash, 2)`
253/// 3. `callable` 字符串 → `call_user_func($rule)`
254/// 4. `default` → `date('Ymd') . '/' . md5(microtime(true) . pathname)`
255///
256/// Rust 端简化为 2 种(Closure/callable 实际业务极少使用):
257/// - [`HashNameRule::Default`]:默认规则
258/// - [`HashNameRule::Hash`]:hash 算法规则
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
260pub enum HashNameRule {
261    /// 默认规则:`date('Ymd') . '/' . md5(microtime(true) . pathname)`
262    ///
263    /// 对齐 PHP 第 195 行
264    #[default]
265    Default,
266
267    /// hash 算法规则:`substr(hash, 0, 2) . '/' . substr(hash, 2)`
268    ///
269    /// 对齐 PHP 第 187-190 行
270    Hash(HashAlgo),
271}
272
273// ============================================================================
274// File 结构
275// ============================================================================
276
277/// 文件类 — 对齐 PHP `think\File`(基于 `SplFileInfo`)
278///
279/// PHP 第 22-46 行:
280/// ```php
281/// class File extends SplFileInfo {
282///     protected $hash = [];
283///     protected $hashName;
284///     protected $extension;
285///     public function __construct(string $path, bool $checkPath = true) {
286///         if ($checkPath && !is_file($path)) {
287///             throw new FileException(sprintf('The file "%s" does not exist', $path));
288///         }
289///         parent::__construct($path);
290///     }
291/// }
292/// ```
293#[derive(Debug, Clone)]
294pub struct File {
295    /// 文件路径(对齐 PHP `SplFileInfo::$path`)
296    path: PathBuf,
297    /// 哈希缓存(对齐 PHP `$hash`,按算法名分组)
298    hash: HashMap<String, String>,
299    /// hashName 缓存(对齐 PHP `$hashName`)
300    hash_name: Option<String>,
301    /// 自定义扩展名(对齐 PHP `$extension`)
302    extension: Option<String>,
303}
304
305impl File {
306    /// 创建 `File` 实例 — 对齐 PHP `File::__construct` 第 39-46 行
307    ///
308    /// `check_path = true` 时检查文件是否存在,不存在返回 [`UploadError::FileNotFound`]。
309    pub fn new<P: AsRef<Path>>(path: P, check_path: bool) -> Result<Self, UploadError> {
310        let path = path.as_ref().to_path_buf();
311        if check_path && !path.is_file() {
312            return Err(UploadError::FileNotFound(
313                path.to_string_lossy().to_string(),
314            ));
315        }
316        Ok(Self {
317            path,
318            hash: HashMap::new(),
319            hash_name: None,
320            extension: None,
321        })
322    }
323
324    /// 不检查路径创建实例 — 对齐 PHP 第 138 行 `new self($target, false)`
325    pub fn new_unchecked<P: AsRef<Path>>(path: P) -> Self {
326        Self {
327            path: path.as_ref().to_path_buf(),
328            hash: HashMap::new(),
329            hash_name: None,
330            extension: None,
331        }
332    }
333
334    /// 获取文件路径 — 对齐 PHP `SplFileInfo::getPathname()`
335    pub fn path(&self) -> &Path {
336        &self.path
337    }
338
339    /// 获取文件路径字符串
340    pub fn path_name(&self) -> String {
341        self.path.to_string_lossy().to_string()
342    }
343
344    /// 获取文件哈希 — 对齐 PHP `File::hash($type)` 第 54-61 行
345    ///
346    /// PHP 行为:
347    /// ```php
348    /// public function hash(string $type = 'sha1'): string {
349    ///     if (!isset($this->hash[$type])) {
350    ///         $this->hash[$type] = hash_file($type, $this->getPathname());
351    ///     }
352    ///     return $this->hash[$type];
353    /// }
354    /// ```
355    pub fn hash(&mut self, algo: HashAlgo) -> Result<String, UploadError> {
356        let key = algo.as_str().to_string();
357        if let Some(h) = self.hash.get(&key) {
358            return Ok(h.clone());
359        }
360        let h = compute_file_hash(&self.path, algo)?;
361        self.hash.insert(key, h.clone());
362        Ok(h)
363    }
364
365    /// 获取文件 MD5 — 对齐 PHP `File::md5()` 第 68-71 行
366    pub fn md5(&mut self) -> Result<String, UploadError> {
367        self.hash(HashAlgo::Md5)
368    }
369
370    /// 获取文件 SHA1 — 对齐 PHP `File::sha1()` 第 78-81 行
371    pub fn sha1(&mut self) -> Result<String, UploadError> {
372        self.hash(HashAlgo::Sha1)
373    }
374
375    /// 获取文件 MIME — 对齐 PHP `File::getMime()` 第 88-93 行
376    ///
377    /// PHP 行为:使用 `finfo_open(FILEINFO_MIME_TYPE)` + `finfo_file()` 检测 MIME。
378    /// Rust 端:优先用 `infer` crate 从内容检测,回退到 `mime_guess` 从扩展名猜测。
379    pub fn get_mime(&self) -> Result<String, UploadError> {
380        // 优先:从内容检测(对齐 PHP finfo_file)
381        if let Ok(Some(t)) = infer::get_from_path(&self.path) {
382            return Ok(t.mime_type().to_string());
383        }
384        // 回退:从扩展名猜测
385        let mime = mime_guess::from_path(&self.path)
386            .first_or_octet_stream()
387            .to_string();
388        Ok(mime)
389    }
390
391    /// 移动文件 — 对齐 PHP `File::move()` 第 102-118 行
392    ///
393    /// PHP 行为:
394    /// 1. 获取 target 文件
395    /// 2. `rename($this->getPathname(), $target)` 移动文件
396    /// 3. 失败抛 `FileException`
397    /// 4. `chmod($target, 0666 & ~umask())` 设置权限
398    /// 5. 返回新 `File` 实例
399    pub fn move_to<P: AsRef<Path>>(
400        &mut self,
401        directory: P,
402        name: Option<&str>,
403    ) -> Result<File, UploadError> {
404        let target = self.get_target_file(directory.as_ref(), name)?;
405
406        fs::rename(&self.path, &target.path).map_err(|e| UploadError::MoveFailed {
407            from: self.path.to_string_lossy().to_string(),
408            to: target.path.to_string_lossy().to_string(),
409            error: e.to_string(),
410        })?;
411
412        // 对齐 PHP `chmod($target, 0666 & ~umask())` 第 115 行
413        #[cfg(unix)]
414        {
415            use std::os::unix::fs::PermissionsExt;
416            let _ = fs::set_permissions(&target.path, fs::Permissions::from_mode(0o666));
417        }
418
419        Ok(target)
420    }
421
422    /// 实例化目标文件 — 对齐 PHP `File::getTargetFile()` 第 126-139 行
423    ///
424    /// PHP 行为:
425    /// 1. 如果目录不存在:`mkdir($directory, 0777, true)`,失败且目录不存在则抛异常
426    /// 2. elseif 目录不可写:抛异常
427    /// 3. target = `rtrim($directory, '/\\') . DIRECTORY_SEPARATOR . (name === null ? basename : getName(name))`
428    /// 4. 返回 `new self($target, false)`
429    fn get_target_file(&self, directory: &Path, name: Option<&str>) -> Result<File, UploadError> {
430        if !directory.is_dir() {
431            fs::create_dir_all(directory).map_err(|_| {
432                UploadError::DirectoryCreateFailed(directory.to_string_lossy().to_string())
433            })?;
434        }
435
436        let file_name = match name {
437            Some(n) => get_name(n)?,
438            None => self
439                .path
440                .file_name()
441                .map(|n| n.to_string_lossy().to_string())
442                .unwrap_or_default(),
443        };
444
445        // 安全检查:拒绝路径遍历攻击(防御性校验,确保 file_name 不含路径分隔符或 ..)
446        if file_name == ".." || file_name.contains('/') || file_name.contains('\\') {
447            return Err(UploadError::InvalidFileName(file_name));
448        }
449
450        let target_path = directory.join(&file_name);
451        Ok(File::new_unchecked(&target_path))
452    }
453
454    /// 文件扩展名 — 对齐 PHP `File::extension()` 第 159-162 行
455    ///
456    /// PHP 行为:`return $this->getExtension();`(`SplFileInfo::getExtension()` 返回最后点后的部分)
457    pub fn extension(&self) -> String {
458        self.path
459            .extension()
460            .map(|e| e.to_string_lossy().to_string())
461            .unwrap_or_default()
462    }
463
464    /// 指定保存文件的扩展名 — 对齐 PHP `File::setExtension()` 第 169-172 行
465    pub fn set_extension(&mut self, extension: &str) {
466        self.extension = Some(extension.to_string());
467    }
468
469    /// 自动生成文件名 — 对齐 PHP `File::hashName($rule)` 第 180-203 行
470    ///
471    /// PHP 行为:
472    /// ```php
473    /// public function hashName($rule = ''): string {
474    ///     if (!$this->hashName) {
475    ///         if ($rule instanceof \Closure) {
476    ///             $this->hashName = call_user_func_array($rule, [$this]);
477    ///         } else {
478    ///             switch (true) {
479    ///                 case in_array($rule, hash_algos()):
480    ///                     $hash = $this->hash($rule);
481    ///                     $this->hashName = substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2);
482    ///                     break;
483    ///                 case is_callable($rule):
484    ///                     $this->hashName = call_user_func($rule);
485    ///                     break;
486    ///                 default:
487    ///                     $this->hashName = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true) . $this->getPathname());
488    ///                     break;
489    ///             }
490    ///         }
491    ///     }
492    ///     $extension = $this->extension ?? $this->extension();
493    ///     return $this->hashName . ($extension ? '.' . $extension : '');
494    /// }
495    /// ```
496    ///
497    /// Rust 端简化:只支持 `Default` 和 `Hash(algo)` 两种规则。
498    pub fn hash_name(&mut self, rule: HashNameRule) -> Result<String, UploadError> {
499        if self.hash_name.is_none() {
500            let hash_name = match rule {
501                HashNameRule::Hash(algo) => {
502                    // 对齐 PHP 第 187-190 行
503                    let hash = self.hash(algo)?;
504                    if hash.len() < 2 {
505                        hash
506                    } else {
507                        format!("{}/{}", &hash[..2], &hash[2..])
508                    }
509                }
510                HashNameRule::Default => {
511                    // 对齐 PHP 第 195 行:date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true) . pathname)
512                    let now = Local::now();
513                    let date_str = now.format("%Y%m%d").to_string();
514                    // microtime(true) 返回浮点数(秒.微秒)
515                    let secs = now.timestamp();
516                    let micros = now.timestamp_subsec_micros();
517                    let microtime_str = format!("{}.{:06}", secs, micros);
518                    let pathname = self.path.to_string_lossy();
519                    let mut md5 = Md5::new();
520                    md5.update(microtime_str.as_bytes());
521                    md5.update(pathname.as_bytes());
522                    let hash = hex::encode(md5.finalize());
523                    format!("{}/{}", date_str, hash)
524                }
525            };
526            self.hash_name = Some(hash_name);
527        }
528
529        // 对齐 PHP 第 201-202 行:$extension = $this->extension ?? $this->extension();
530        let extension = match &self.extension {
531            Some(ext) => ext.clone(),
532            None => self.extension(),
533        };
534
535        let hash_name = self
536            .hash_name
537            .as_ref()
538            .expect("hash_name 已在上方初始化")
539            .clone();
540        if extension.is_empty() {
541            Ok(hash_name)
542        } else {
543            // 对齐 PHP 第 202 行:$this->hashName . ($extension ? '.' . $extension : '')
544            Ok(format!("{}.{}", hash_name, extension))
545        }
546    }
547
548    /// 获取文件名(不含目录) — 对齐 PHP `SplFileInfo::getBasename()`
549    pub fn basename(&self) -> String {
550        self.path
551            .file_name()
552            .map(|n| n.to_string_lossy().to_string())
553            .unwrap_or_default()
554    }
555}
556
557// ============================================================================
558// UploadedFile 结构
559// ============================================================================
560
561/// 上传文件类 — 对齐 PHP `think\file\UploadedFile`
562///
563/// PHP 第 18-34 行:
564/// ```php
565/// class UploadedFile extends File {
566///     private $test = false;
567///     private $originalName;
568///     private $mimeType;
569///     private $error;
570///     public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) {
571///         $this->originalName = $originalName;
572///         $this->mimeType     = $mimeType ?: 'application/octet-stream';
573///         $this->test         = $test;
574///         $this->error        = $error ?: UPLOAD_ERR_OK;
575///         parent::__construct($path, UPLOAD_ERR_OK === $this->error);
576///     }
577/// }
578/// ```
579#[derive(Debug, Clone)]
580pub struct UploadedFile {
581    /// 父类 `File`
582    file: File,
583    /// 测试模式(对齐 PHP `$test`)
584    test: bool,
585    /// 原始文件名(对齐 PHP `$originalName`)
586    original_name: String,
587    /// MIME 类型(对齐 PHP `$mimeType`)
588    mime_type: String,
589    /// 上传错误码(对齐 PHP `$error`)
590    error: UploadErrCode,
591}
592
593impl UploadedFile {
594    /// 创建 `UploadedFile` 实例 — 对齐 PHP `UploadedFile::__construct` 第 26-34 行
595    pub fn new<P: AsRef<Path>>(
596        path: P,
597        original_name: &str,
598        mime_type: Option<&str>,
599        error: Option<i32>,
600        test: bool,
601    ) -> Result<Self, UploadError> {
602        let error = UploadErrCode::from_i32(error.unwrap_or(0));
603        let mime = mime_type.unwrap_or("application/octet-stream").to_string();
604
605        // 对齐 PHP 第 33 行:UPLOAD_ERR_OK === $this->error 时 checkPath=true
606        let check_path = error == UploadErrCode::Ok;
607        let file = File::new(path, check_path)?;
608
609        Ok(Self {
610            file,
611            test,
612            original_name: original_name.to_string(),
613            mime_type: mime,
614            error,
615        })
616    }
617
618    /// 验证上传文件 — 对齐 PHP `UploadedFile::isValid()` 第 36-41 行
619    ///
620    /// PHP 行为:
621    /// ```php
622    /// public function isValid(): bool {
623    ///     $isOk = UPLOAD_ERR_OK === $this->error;
624    ///     return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
625    /// }
626    /// ```
627    ///
628    /// Rust 端:`is_uploaded_file` 是 PHP SAPI 函数,无法精确对齐。
629    /// 简化为检查文件是否存在(非 test 模式)。
630    pub fn is_valid(&self) -> bool {
631        let is_ok = self.error == UploadErrCode::Ok;
632        if self.test {
633            is_ok
634        } else {
635            // 对齐 PHP `is_uploaded_file($pathname)`
636            is_ok && self.file.path().is_file()
637        }
638    }
639
640    /// 移动上传文件 — 对齐 PHP `UploadedFile::move()` 第 50-75 行
641    ///
642    /// PHP 行为:
643    /// 1. `isValid()` 失败 → 抛 `FileException($this->getErrorMessage())`
644    /// 2. `test` 模式:调用 `parent::move()`(即 `rename`)
645    /// 3. 非 test 模式:`move_uploaded_file($pathname, $target)`
646    /// 4. 失败抛 `FileException`
647    /// 5. `chmod($target, 0666 & ~umask())`
648    /// 6. 返回新 `File`
649    pub fn move_to<P: AsRef<Path>>(
650        &mut self,
651        directory: P,
652        name: Option<&str>,
653    ) -> Result<File, UploadError> {
654        if !self.is_valid() {
655            return Err(UploadError::UploadFailed(
656                self.error.error_message().to_string(),
657            ));
658        }
659
660        if self.test {
661            // 对齐 PHP 第 54 行:`return parent::move($directory, $name);`
662            return self.file.move_to(directory, name);
663        }
664
665        // 对齐 PHP 第 63 行:`move_uploaded_file($this->getPathname(), $target)`
666        // Rust 端:使用 `fs::rename`(无 SAPI 等价物)
667        let target = self.file.get_target_file(directory.as_ref(), name)?;
668        fs::rename(self.file.path(), &target.path).map_err(|e| UploadError::MoveFailed {
669            from: self.file.path().to_string_lossy().to_string(),
670            to: target.path.to_string_lossy().to_string(),
671            error: e.to_string(),
672        })?;
673
674        // 对齐 PHP 第 69 行:`chmod($target, 0666 & ~umask())`
675        #[cfg(unix)]
676        {
677            use std::os::unix::fs::PermissionsExt;
678            let _ = fs::set_permissions(&target.path, fs::Permissions::from_mode(0o666));
679        }
680
681        Ok(target)
682    }
683
684    /// 获取原始 MIME — 对齐 PHP `UploadedFile::getOriginalMime()` 第 112-115 行
685    pub fn original_mime(&self) -> &str {
686        &self.mime_type
687    }
688
689    /// 获取原始文件名 — 对齐 PHP `UploadedFile::getOriginalName()` 第 121-124 行
690    pub fn original_name(&self) -> &str {
691        &self.original_name
692    }
693
694    /// 获取原始扩展名 — 对齐 PHP `UploadedFile::getOriginalExtension()` 第 130-133 行
695    ///
696    /// PHP 行为:`return pathinfo($this->originalName, PATHINFO_EXTENSION);`
697    pub fn original_extension(&self) -> String {
698        Path::new(&self.original_name)
699            .extension()
700            .map(|e| e.to_string_lossy().to_string())
701            .unwrap_or_default()
702    }
703
704    /// 获取文件扩展名 — 对齐 PHP `UploadedFile::extension()` 第 139-142 行
705    ///
706    /// PHP 行为:覆写父类,返回原始扩展名。
707    /// ```php
708    /// public function extension(): string {
709    ///     return $this->getOriginalExtension();
710    /// }
711    /// ```
712    pub fn extension(&self) -> String {
713        self.original_extension()
714    }
715
716    /// 获取错误信息 — 对齐 PHP `UploadedFile::getErrorMessage()` 第 82-106 行
717    pub fn error_message(&self) -> &'static str {
718        self.error.error_message()
719    }
720
721    /// 获取错误码
722    pub fn error_code(&self) -> UploadErrCode {
723        self.error
724    }
725
726    /// 访问内部 `File`(不可变)
727    pub fn as_file(&self) -> &File {
728        &self.file
729    }
730
731    /// 访问内部 `File`(可变)
732    pub fn as_file_mut(&mut self) -> &mut File {
733        &mut self.file
734    }
735}
736
737// ============================================================================
738// 辅助函数
739// ============================================================================
740
741/// 获取文件名 — 对齐 PHP `File::getName($name)` 第 146-153 行
742///
743/// PHP 行为:
744/// ```php
745/// protected function getName(string $name): string {
746///     $originalName = str_replace('\\', '/', $name);
747///     $pos          = strrpos($originalName, '/');
748///     $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
749///     return $originalName;
750/// }
751/// ```
752fn get_name(name: &str) -> Result<String, UploadError> {
753    // 安全检查:拒绝路径遍历攻击(包含 .. 的文件名视为非法)
754    if name.contains("..") {
755        return Err(UploadError::InvalidFileName(name.to_string()));
756    }
757    // 对齐 PHP `str_replace('\\', '/', $name)`
758    let original_name = name.replace('\\', "/");
759    // 对齐 PHP `strrpos($originalName, '/')`
760    match original_name.rfind('/') {
761        Some(pos) => Ok(original_name[pos + 1..].to_string()),
762        None => Ok(original_name),
763    }
764}
765
766/// 计算文件哈希 — 对齐 PHP `hash_file($type, $pathname)`
767fn compute_file_hash(path: &Path, algo: HashAlgo) -> Result<String, UploadError> {
768    let mut file = fs::File::open(path)?;
769    let mut buf = Vec::new();
770    file.read_to_end(&mut buf)?;
771
772    let hash = match algo {
773        HashAlgo::Md5 => {
774            let mut h = Md5::new();
775            h.update(&buf);
776            hex::encode(h.finalize())
777        }
778        HashAlgo::Sha1 => {
779            let mut h = sha1::Sha1::new();
780            h.update(&buf);
781            hex::encode(h.finalize())
782        }
783        HashAlgo::Sha256 => {
784            let mut h = sha2::Sha256::new();
785            h.update(&buf);
786            hex::encode(h.finalize())
787        }
788        HashAlgo::Sha512 => {
789            let mut h = sha2::Sha512::new();
790            h.update(&buf);
791            hex::encode(h.finalize())
792        }
793    };
794    Ok(hash)
795}
796
797// ============================================================================
798// multipart/form-data 上传机制 — 对齐 PHP `Request::file()` + `$_FILES`
799// ============================================================================
800
801use axum::extract::Multipart;
802use std::io::Write;
803use tempfile::NamedTempFile;
804
805/// multipart 解析结果 — 对齐 PHP `$_FILES` + `$_POST`
806#[derive(Debug, Default)]
807pub struct MultipartResult {
808    /// 文件字段(字段名 → `UploadedFile` 列表)
809    ///
810    /// 对齐 PHP `$_FILES`,每个字段可能有多个文件(多文件上传)
811    pub files: HashMap<String, Vec<UploadedFile>>,
812
813    /// 普通字段(字段名 → 值)
814    ///
815    /// 对齐 PHP `$_POST`
816    pub fields: HashMap<String, String>,
817}
818
819impl MultipartResult {
820    /// 获取单个文件(对齐 PHP `Request::file($name)` 返回单个文件)
821    ///
822    /// PHP 行为:
823    /// - name 为空 → 返回全部
824    /// - name 存在 → 返回该字段的第一个文件
825    pub fn file(&self, name: &str) -> Option<&UploadedFile> {
826        self.files.get(name).and_then(|list| list.first())
827    }
828
829    /// 获取字段的所有文件(多文件上传)
830    pub fn files(&self, name: &str) -> Option<&Vec<UploadedFile>> {
831        self.files.get(name)
832    }
833
834    /// 获取普通字段值(对齐 PHP `$_POST[$name]`)
835    pub fn field(&self, name: &str) -> Option<&str> {
836        self.fields.get(name).map(|s| s.as_str())
837    }
838
839    /// 获取文件数量
840    pub fn file_count(&self) -> usize {
841        self.files.values().map(|v| v.len()).sum()
842    }
843
844    /// 是否为空
845    pub fn is_empty(&self) -> bool {
846        self.files.is_empty() && self.fields.is_empty()
847    }
848}
849
850/// 解析 `multipart/form-data` 请求 — 对齐 PHP `Request::file()` + `$_FILES`
851///
852/// PHP 行为:
853/// - `$_FILES` 自动填充上传文件信息(name/type/tmp_name/error/size)
854/// - `Request::file($name)` 包装为 `UploadedFile` 对象
855/// - `$_POST` 填充普通字段
856///
857/// Rust 端:使用 `axum::extract::Multipart` 提取字段,
858/// 文件字段保存到临时文件并创建 `UploadedFile`,普通字段保存到 `fields`。
859///
860/// ## 用法
861///
862/// ```ignore
863/// use sz_rust_core::upload::parse_multipart;
864/// use axum::extract::Multipart;
865///
866/// async fn upload_handler(mut multipart: Multipart) -> Result<String, String> {
867///     let result = parse_multipart(&mut multipart).await
868///         .map_err(|e| e.to_string())?;
869///     
870///     if let Some(uploaded) = result.file("avatar") {
871///         let moved = uploaded.move_to("/var/www/uploads", Some("avatar.png"))
872///             .map_err(|e| e.to_string())?;
873///         return Ok(format!("上传成功:{:?}", moved.path()));
874///     }
875///     
876///     Ok("未找到文件".to_string())
877/// }
878/// ```
879pub async fn parse_multipart(multipart: &mut Multipart) -> Result<MultipartResult, UploadError> {
880    let mut result = MultipartResult::default();
881
882    while let Some(field) = multipart
883        .next_field()
884        .await
885        .map_err(|e| UploadError::UploadFailed(e.to_string()))?
886    {
887        let name = field.name().unwrap_or("").to_string();
888        let file_name = field.file_name().map(|s| s.to_string());
889        let content_type = field.content_type().map(|s| s.to_string());
890
891        let data = field
892            .bytes()
893            .await
894            .map_err(|e| UploadError::UploadFailed(e.to_string()))?;
895
896        if let Some(file_name) = file_name {
897            // 文件字段:保存到临时文件
898            let ext = Path::new(&file_name)
899                .extension()
900                .map(|e| format!(".{}", e.to_string_lossy()))
901                .unwrap_or_default();
902
903            let mut temp = NamedTempFile::with_suffix(&ext)?;
904            temp.write_all(&data)?;
905
906            // keep() 让文件持久化(不被 drop 删除),返回 (path, _file)
907            // 对齐 PHP `$_FILES[xxx]['tmp_name']` — 由 SAPI 创建,请求结束前不会删除
908            let (_file, path) = temp.keep()?;
909
910            let uploaded = UploadedFile::new(
911                &path,
912                &file_name,
913                content_type.as_deref(),
914                Some(0),
915                // test 模式:使用 rename(对齐 PHP `parent::move`)
916                // 因为 axum 上传的文件不是 PHP SAPI 上传的,无法使用 move_uploaded_file
917                true,
918            )?;
919
920            result.files.entry(name).or_default().push(uploaded);
921        } else {
922            // 普通字段:保存为字符串
923            let value = String::from_utf8_lossy(&data).to_string();
924            result.fields.insert(name, value);
925        }
926    }
927
928    Ok(result)
929}
930
931// ============================================================================
932
933// 单元测试
934// ============================================================================
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939    use std::io::Write;
940    use tempfile::NamedTempFile;
941
942    /// 创建临时文件并写入内容
943    fn create_temp_file(content: &[u8], suffix: &str) -> NamedTempFile {
944        let mut file = NamedTempFile::with_suffix(suffix).expect("创建临时文件失败");
945        file.write_all(content).expect("写入临时文件失败");
946        file
947    }
948
949    // ------------------------------------------------------------------------
950    // 组 1:UploadErrCode 测试
951    // ------------------------------------------------------------------------
952
953    #[test]
954    fn test_upload_err_code_from_i32() {
955        assert_eq!(UploadErrCode::from_i32(0), UploadErrCode::Ok);
956        assert_eq!(UploadErrCode::from_i32(1), UploadErrCode::IniSize);
957        assert_eq!(UploadErrCode::from_i32(2), UploadErrCode::FormSize);
958        assert_eq!(UploadErrCode::from_i32(3), UploadErrCode::Partial);
959        assert_eq!(UploadErrCode::from_i32(4), UploadErrCode::NoFile);
960        assert_eq!(UploadErrCode::from_i32(6), UploadErrCode::NoTmpDir);
961        assert_eq!(UploadErrCode::from_i32(7), UploadErrCode::CantWrite);
962        // 未知错误码回退到 Ok
963        assert_eq!(UploadErrCode::from_i32(99), UploadErrCode::Ok);
964    }
965
966    #[test]
967    fn test_upload_err_code_error_message() {
968        // 对齐 PHP 第 84-85 行:1/2 → size exceeds
969        assert_eq!(
970            UploadErrCode::IniSize.error_message(),
971            "upload File size exceeds the maximum value"
972        );
973        assert_eq!(
974            UploadErrCode::FormSize.error_message(),
975            "upload File size exceeds the maximum value"
976        );
977        // 对齐 PHP 第 88-89 行:3 → portion
978        assert_eq!(
979            UploadErrCode::Partial.error_message(),
980            "only the portion of file is uploaded"
981        );
982        // 对齐 PHP 第 91-92 行:4 → no file
983        assert_eq!(UploadErrCode::NoFile.error_message(), "no file to uploaded");
984        // 对齐 PHP 第 94-95 行:6 → temp dir
985        assert_eq!(
986            UploadErrCode::NoTmpDir.error_message(),
987            "upload temp dir not found"
988        );
989        // 对齐 PHP 第 97-98 行:7 → write error
990        assert_eq!(UploadErrCode::CantWrite.error_message(), "file write error");
991        // 对齐 PHP 第 101-102 行:default → unknown
992        assert_eq!(UploadErrCode::Ok.error_message(), "unknown upload error");
993    }
994
995    // ------------------------------------------------------------------------
996    // 组 2:HashAlgo 测试
997    // ------------------------------------------------------------------------
998
999    #[test]
1000    fn test_hash_algo_as_str() {
1001        assert_eq!(HashAlgo::Md5.as_str(), "md5");
1002        assert_eq!(HashAlgo::Sha1.as_str(), "sha1");
1003        assert_eq!(HashAlgo::Sha256.as_str(), "sha256");
1004        assert_eq!(HashAlgo::Sha512.as_str(), "sha512");
1005    }
1006
1007    #[test]
1008    fn test_hash_algo_parse_algo() {
1009        assert_eq!(HashAlgo::parse_algo("md5"), Some(HashAlgo::Md5));
1010        assert_eq!(HashAlgo::parse_algo("sha1"), Some(HashAlgo::Sha1));
1011        assert_eq!(HashAlgo::parse_algo("sha256"), Some(HashAlgo::Sha256));
1012        assert_eq!(HashAlgo::parse_algo("sha512"), Some(HashAlgo::Sha512));
1013        // 对齐 PHP `in_array($rule, hash_algos())` 找不到返回 None
1014        assert_eq!(HashAlgo::parse_algo("unknown"), None);
1015    }
1016
1017    // ------------------------------------------------------------------------
1018    // 组 3:File 基础测试
1019    // ------------------------------------------------------------------------
1020
1021    #[test]
1022    fn test_file_new_with_check_path() {
1023        // 文件存在
1024        let temp = create_temp_file(b"hello", ".txt");
1025        let file = File::new(temp.path(), true);
1026        assert!(file.is_ok());
1027
1028        // 文件不存在
1029        let file = File::new("/nonexistent/file.txt", true);
1030        assert!(matches!(file, Err(UploadError::FileNotFound(_))));
1031    }
1032
1033    #[test]
1034    fn test_file_new_without_check_path() {
1035        // check_path = false 时不检查文件存在性
1036        let file = File::new("/nonexistent/file.txt", false);
1037        assert!(file.is_ok());
1038    }
1039
1040    #[test]
1041    fn test_file_new_unchecked() {
1042        let file = File::new_unchecked("/some/path/file.txt");
1043        assert_eq!(file.path(), Path::new("/some/path/file.txt"));
1044    }
1045
1046    #[test]
1047    fn test_file_path() {
1048        let temp = create_temp_file(b"hello", ".txt");
1049        let file = File::new(temp.path(), true).unwrap();
1050        assert_eq!(file.path(), temp.path());
1051    }
1052
1053    #[test]
1054    fn test_file_path_name() {
1055        let temp = create_temp_file(b"hello", ".txt");
1056        let file = File::new(temp.path(), true).unwrap();
1057        assert_eq!(file.path_name(), temp.path().to_string_lossy().to_string());
1058    }
1059
1060    #[test]
1061    fn test_file_extension() {
1062        // 对齐 PHP `SplFileInfo::getExtension()`
1063        let temp = create_temp_file(b"hello", ".txt");
1064        let file = File::new(temp.path(), true).unwrap();
1065        assert_eq!(file.extension(), "txt");
1066    }
1067
1068    #[test]
1069    fn test_file_extension_no_extension() {
1070        // 无扩展名返回空字符串
1071        let mut file = NamedTempFile::new().unwrap();
1072        file.write_all(b"hello").unwrap();
1073        let file = File::new(file.path(), true).unwrap();
1074        assert_eq!(file.extension(), "");
1075    }
1076
1077    #[test]
1078    fn test_file_set_extension() {
1079        // 对齐 PHP `File::setExtension($extension)`
1080        let temp = create_temp_file(b"hello", ".txt");
1081        let mut file = File::new(temp.path(), true).unwrap();
1082        assert_eq!(file.extension(), "txt");
1083        file.set_extension("jpg");
1084        assert_eq!(file.extension, Some("jpg".to_string()));
1085    }
1086
1087    #[test]
1088    fn test_file_basename() {
1089        // 对齐 PHP `SplFileInfo::getBasename()`
1090        let temp = create_temp_file(b"hello", ".txt");
1091        let file = File::new(temp.path(), true).unwrap();
1092        let basename = file.basename();
1093        assert!(basename.ends_with(".txt"));
1094    }
1095
1096    // ------------------------------------------------------------------------
1097    // 组 4:File hash 测试
1098    // ------------------------------------------------------------------------
1099
1100    #[test]
1101    fn test_file_md5() {
1102        // 对齐 PHP `File::md5()` 第 68-71 行
1103        let temp = create_temp_file(b"hello", ".txt");
1104        let mut file = File::new(temp.path(), true).unwrap();
1105        let md5 = file.md5().unwrap();
1106        // "hello" 的 MD5
1107        assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
1108    }
1109
1110    #[test]
1111    fn test_file_sha1() {
1112        // 对齐 PHP `File::sha1()` 第 78-81 行
1113        let temp = create_temp_file(b"hello", ".txt");
1114        let mut file = File::new(temp.path(), true).unwrap();
1115        let sha1 = file.sha1().unwrap();
1116        // "hello" 的 SHA1
1117        assert_eq!(sha1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1118    }
1119
1120    #[test]
1121    fn test_file_hash_md5() {
1122        // 对齐 PHP `File::hash('md5')`
1123        let temp = create_temp_file(b"hello", ".txt");
1124        let mut file = File::new(temp.path(), true).unwrap();
1125        let hash = file.hash(HashAlgo::Md5).unwrap();
1126        assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
1127    }
1128
1129    #[test]
1130    fn test_file_hash_sha1() {
1131        // 对齐 PHP `File::hash('sha1')`
1132        let temp = create_temp_file(b"hello", ".txt");
1133        let mut file = File::new(temp.path(), true).unwrap();
1134        let hash = file.hash(HashAlgo::Sha1).unwrap();
1135        assert_eq!(hash, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1136    }
1137
1138    #[test]
1139    fn test_file_hash_sha256() {
1140        // 对齐 PHP `File::hash('sha256')`
1141        let temp = create_temp_file(b"hello", ".txt");
1142        let mut file = File::new(temp.path(), true).unwrap();
1143        let hash = file.hash(HashAlgo::Sha256).unwrap();
1144        // "hello" 的 SHA256
1145        assert_eq!(
1146            hash,
1147            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
1148        );
1149    }
1150
1151    #[test]
1152    fn test_file_hash_sha512() {
1153        // 对齐 PHP `File::hash('sha512')`
1154        let temp = create_temp_file(b"hello", ".txt");
1155        let mut file = File::new(temp.path(), true).unwrap();
1156        let hash = file.hash(HashAlgo::Sha512).unwrap();
1157        // "hello" 的 SHA512(前 32 字符)
1158        assert!(hash.starts_with("9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca"));
1159    }
1160
1161    #[test]
1162    fn test_file_hash_caching() {
1163        // 对齐 PHP 第 56-58 行:缓存机制
1164        let temp = create_temp_file(b"hello", ".txt");
1165        let mut file = File::new(temp.path(), true).unwrap();
1166        let hash1 = file.hash(HashAlgo::Md5).unwrap();
1167        let hash2 = file.hash(HashAlgo::Md5).unwrap();
1168        assert_eq!(hash1, hash2);
1169        // 缓存验证
1170        assert!(file.hash.contains_key("md5"));
1171    }
1172
1173    #[test]
1174    fn test_file_hash_multiple_algos() {
1175        // 多算法独立缓存
1176        let temp = create_temp_file(b"hello", ".txt");
1177        let mut file = File::new(temp.path(), true).unwrap();
1178        let md5 = file.hash(HashAlgo::Md5).unwrap();
1179        let sha1 = file.hash(HashAlgo::Sha1).unwrap();
1180        assert_ne!(md5, sha1);
1181        assert!(file.hash.contains_key("md5"));
1182        assert!(file.hash.contains_key("sha1"));
1183    }
1184
1185    // ------------------------------------------------------------------------
1186    // 组 5:File getMime 测试
1187    // ------------------------------------------------------------------------
1188
1189    #[test]
1190    fn test_file_get_mime_text() {
1191        // 对齐 PHP `File::getMime()` 第 88-93 行
1192        let temp = create_temp_file(b"hello", ".txt");
1193        let file = File::new(temp.path(), true).unwrap();
1194        let mime = file.get_mime().unwrap();
1195        // 内容检测优先(infer 检测文本),扩展名回退
1196        // 文本文件内容检测可能返回 text/plain 或 application/octet-stream
1197        assert!(
1198            mime == "text/plain" || mime == "application/octet-stream",
1199            "mime = {}",
1200            mime
1201        );
1202    }
1203
1204    #[test]
1205    fn test_file_get_mime_png() {
1206        // PNG 文件内容检测
1207        let png_header = [
1208            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1209            0x00, 0x00, 0x00, 0x0D, // IHDR length
1210            0x49, 0x48, 0x44, 0x52, // "IHDR"
1211        ];
1212        let mut file = NamedTempFile::with_suffix(".png").unwrap();
1213        file.write_all(&png_header).unwrap();
1214        let file = File::new(file.path(), true).unwrap();
1215        let mime = file.get_mime().unwrap();
1216        assert_eq!(mime, "image/png");
1217    }
1218
1219    #[test]
1220    fn test_file_get_mime_jpg() {
1221        // JPEG 文件内容检测(FF D8 开头)
1222        let jpg_header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
1223        let mut file = NamedTempFile::with_suffix(".jpg").unwrap();
1224        file.write_all(&jpg_header).unwrap();
1225        let file = File::new(file.path(), true).unwrap();
1226        let mime = file.get_mime().unwrap();
1227        assert_eq!(mime, "image/jpeg");
1228    }
1229
1230    #[test]
1231    fn test_file_get_mime_unknown_extension() {
1232        // 无扩展名 + 未知内容 → 回退 application/octet-stream
1233        let temp = create_temp_file(&[0x00, 0x01, 0x02, 0x03], "");
1234        let file = File::new(temp.path(), true).unwrap();
1235        let mime = file.get_mime().unwrap();
1236        assert_eq!(mime, "application/octet-stream");
1237    }
1238
1239    // ------------------------------------------------------------------------
1240    // 组 6:File move 测试
1241    // ------------------------------------------------------------------------
1242
1243    #[test]
1244    fn test_file_move_with_default_name() {
1245        // 对齐 PHP `File::move($directory, null)` — 使用原文件名
1246        let temp = create_temp_file(b"hello", ".txt");
1247        let temp_dir = tempfile::tempdir().unwrap();
1248        let target_dir = temp_dir.path().join("subdir");
1249
1250        let mut file = File::new(temp.path(), true).unwrap();
1251        let original_basename = file.basename();
1252        let moved = file.move_to(&target_dir, None).unwrap();
1253
1254        assert!(moved.path().is_file());
1255        assert_eq!(moved.basename(), original_basename);
1256        // 原文件已移动
1257        assert!(!temp.path().exists());
1258    }
1259
1260    #[test]
1261    fn test_file_move_with_custom_name() {
1262        // 对齐 PHP `File::move($directory, $name)`
1263        let temp = create_temp_file(b"hello", ".txt");
1264        let temp_dir = tempfile::tempdir().unwrap();
1265
1266        let mut file = File::new(temp.path(), true).unwrap();
1267        let moved = file.move_to(&temp_dir, Some("custom.txt")).unwrap();
1268
1269        assert!(moved.path().is_file());
1270        assert_eq!(moved.basename(), "custom.txt");
1271    }
1272
1273    #[test]
1274    fn test_file_move_creates_directory() {
1275        // 对齐 PHP `mkdir($directory, 0777, true)` — 递归创建目录
1276        let temp = create_temp_file(b"hello", ".txt");
1277        let temp_dir = tempfile::tempdir().unwrap();
1278        let nested_dir = temp_dir.path().join("a").join("b").join("c");
1279
1280        let mut file = File::new(temp.path(), true).unwrap();
1281        let moved = file.move_to(&nested_dir, Some("file.txt")).unwrap();
1282
1283        assert!(moved.path().is_file());
1284        assert!(nested_dir.is_dir());
1285    }
1286
1287    #[test]
1288    fn test_file_move_preserves_content() {
1289        let temp = create_temp_file(b"hello world", ".txt");
1290        let temp_dir = tempfile::tempdir().unwrap();
1291
1292        let mut file = File::new(temp.path(), true).unwrap();
1293        let moved = file.move_to(&temp_dir, Some("moved.txt")).unwrap();
1294
1295        let content = std::fs::read_to_string(moved.path()).unwrap();
1296        assert_eq!(content, "hello world");
1297    }
1298
1299    // ------------------------------------------------------------------------
1300    // 组 7:File hashName 测试
1301    // ------------------------------------------------------------------------
1302
1303    #[test]
1304    fn test_file_hash_name_default_format() {
1305        // 对齐 PHP `File::hashName()` 默认规则:date('Ymd')/md5(microtime.pathname).ext
1306        let temp = create_temp_file(b"hello", ".txt");
1307        let mut file = File::new(temp.path(), true).unwrap();
1308        let hash_name = file.hash_name(HashNameRule::Default).unwrap();
1309
1310        // 格式:YYYYMMDD/32位md5.txt
1311        let parts: Vec<&str> = hash_name.split('/').collect();
1312        assert_eq!(parts.len(), 2);
1313        let (date_part, md5_ext) = (parts[0], parts[1]);
1314
1315        // 日期部分:8 位数字
1316        assert_eq!(date_part.len(), 8);
1317        assert!(date_part.chars().all(|c| c.is_ascii_digit()));
1318
1319        // md5.ext 部分
1320        let ext_parts: Vec<&str> = md5_ext.split('.').collect();
1321        assert_eq!(ext_parts.len(), 2);
1322        assert_eq!(ext_parts[0].len(), 32); // MD5 长度
1323        assert!(ext_parts[0].chars().all(|c| c.is_ascii_hexdigit()));
1324        assert_eq!(ext_parts[1], "txt"); // 扩展名
1325    }
1326
1327    #[test]
1328    fn test_file_hash_name_default_no_extension() {
1329        // 无扩展名 → 不追加 .ext
1330        let temp = NamedTempFile::new().unwrap();
1331        std::fs::write(temp.path(), b"hello").unwrap();
1332        let mut file = File::new(temp.path(), true).unwrap();
1333        let hash_name = file.hash_name(HashNameRule::Default).unwrap();
1334
1335        // 格式:YYYYMMDD/32位md5(无 .ext)
1336        let parts: Vec<&str> = hash_name.split('/').collect();
1337        assert_eq!(parts.len(), 2);
1338        assert!(!parts[1].contains('.'));
1339    }
1340
1341    #[test]
1342    fn test_file_hash_name_hash_md5() {
1343        // 对齐 PHP `File::hashName('md5')`:substr(hash, 0, 2)/substr(hash, 2).ext
1344        let temp = create_temp_file(b"hello", ".txt");
1345        let mut file = File::new(temp.path(), true).unwrap();
1346        let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
1347
1348        // 格式:5d/41402abc4b2a76b9719d911017c592.txt
1349        assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592.txt");
1350    }
1351
1352    #[test]
1353    fn test_file_hash_name_hash_sha1() {
1354        // 对齐 PHP `File::hashName('sha1')`
1355        let temp = create_temp_file(b"hello", ".txt");
1356        let mut file = File::new(temp.path(), true).unwrap();
1357        let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
1358
1359        // "hello" 的 SHA1 = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
1360        assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d.txt");
1361    }
1362
1363    #[test]
1364    fn test_file_hash_name_caching() {
1365        // 对齐 PHP `if (!$this->hashName)` — 缓存机制
1366        let temp = create_temp_file(b"hello", ".txt");
1367        let mut file = File::new(temp.path(), true).unwrap();
1368        let name1 = file.hash_name(HashNameRule::Default).unwrap();
1369        let name2 = file.hash_name(HashNameRule::Default).unwrap();
1370        assert_eq!(name1, name2);
1371    }
1372
1373    #[test]
1374    fn test_file_hash_name_with_set_extension() {
1375        // 对齐 PHP `$extension = $this->extension ?? $this->extension();`
1376        // setExtension 覆盖
1377        let temp = create_temp_file(b"hello", ".txt");
1378        let mut file = File::new(temp.path(), true).unwrap();
1379        file.set_extension("jpg");
1380        let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
1381        assert!(hash_name.ends_with(".jpg"));
1382    }
1383
1384    // ------------------------------------------------------------------------
1385    // 组 8:UploadedFile 基础测试
1386    // ------------------------------------------------------------------------
1387
1388    #[test]
1389    fn test_uploaded_file_new_ok() {
1390        // 对齐 PHP `UploadedFile::__construct` 第 26-34 行
1391        let temp = create_temp_file(b"hello", ".txt");
1392        let uploaded = UploadedFile::new(
1393            temp.path(),
1394            "original.txt",
1395            Some("text/plain"),
1396            Some(0),
1397            false,
1398        );
1399        assert!(uploaded.is_ok());
1400    }
1401
1402    #[test]
1403    fn test_uploaded_file_new_with_error() {
1404        // 错误码非 0 时不检查路径存在性
1405        let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(3), false);
1406        assert!(uploaded.is_ok());
1407    }
1408
1409    #[test]
1410    fn test_uploaded_file_new_check_path_on_ok() {
1411        // UPLOAD_ERR_OK 时检查路径存在性
1412        let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(0), false);
1413        assert!(matches!(uploaded, Err(UploadError::FileNotFound(_))));
1414    }
1415
1416    #[test]
1417    fn test_uploaded_file_original_name() {
1418        let temp = create_temp_file(b"hello", ".txt");
1419        let uploaded = UploadedFile::new(
1420            temp.path(),
1421            "my_file.txt",
1422            Some("text/plain"),
1423            Some(0),
1424            false,
1425        )
1426        .unwrap();
1427        assert_eq!(uploaded.original_name(), "my_file.txt");
1428    }
1429
1430    #[test]
1431    fn test_uploaded_file_original_mime() {
1432        let temp = create_temp_file(b"hello", ".txt");
1433        let uploaded = UploadedFile::new(
1434            temp.path(),
1435            "my_file.txt",
1436            Some("text/plain"),
1437            Some(0),
1438            false,
1439        )
1440        .unwrap();
1441        assert_eq!(uploaded.original_mime(), "text/plain");
1442    }
1443
1444    #[test]
1445    fn test_uploaded_file_original_mime_default() {
1446        // 对齐 PHP `$mimeType ?: 'application/octet-stream'`
1447        let temp = create_temp_file(b"hello", ".txt");
1448        let uploaded = UploadedFile::new(temp.path(), "my_file.txt", None, Some(0), false).unwrap();
1449        assert_eq!(uploaded.original_mime(), "application/octet-stream");
1450    }
1451
1452    #[test]
1453    fn test_uploaded_file_original_extension() {
1454        // 对齐 PHP `pathinfo($originalName, PATHINFO_EXTENSION)`
1455        let temp = create_temp_file(b"hello", ".txt");
1456        let uploaded = UploadedFile::new(
1457            temp.path(),
1458            "my_file.txt",
1459            Some("text/plain"),
1460            Some(0),
1461            false,
1462        )
1463        .unwrap();
1464        assert_eq!(uploaded.original_extension(), "txt");
1465    }
1466
1467    #[test]
1468    fn test_uploaded_file_original_extension_no_ext() {
1469        let temp = create_temp_file(b"hello", ".txt");
1470        let uploaded = UploadedFile::new(
1471            temp.path(),
1472            "no_extension",
1473            Some("text/plain"),
1474            Some(0),
1475            false,
1476        )
1477        .unwrap();
1478        assert_eq!(uploaded.original_extension(), "");
1479    }
1480
1481    #[test]
1482    fn test_uploaded_file_extension_overrides_parent() {
1483        // 对齐 PHP 第 139-142 行:extension() 覆写父类,返回 original_extension
1484        let temp = create_temp_file(b"hello", ".txt");
1485        // 注意:原始文件名扩展名是 .jpg,文件路径扩展名是 .txt
1486        let uploaded =
1487            UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
1488                .unwrap();
1489        // extension() 应返回 jpg(原始扩展名),而不是 txt(路径扩展名)
1490        assert_eq!(uploaded.extension(), "jpg");
1491        // 但 as_file().extension() 返回 txt(父类行为)
1492        assert_eq!(uploaded.as_file().extension(), "txt");
1493    }
1494
1495    // ------------------------------------------------------------------------
1496    // 组 9:UploadedFile isValid 测试
1497    // ------------------------------------------------------------------------
1498
1499    #[test]
1500    fn test_uploaded_file_is_valid_ok() {
1501        // 对齐 PHP `isValid()` — UPLOAD_ERR_OK && is_uploaded_file
1502        let temp = create_temp_file(b"hello", ".txt");
1503        let uploaded = UploadedFile::new(
1504            temp.path(),
1505            "my_file.txt",
1506            Some("text/plain"),
1507            Some(0),
1508            false,
1509        )
1510        .unwrap();
1511        // Rust 端:is_uploaded_file 简化为文件存在性检查
1512        assert!(uploaded.is_valid());
1513    }
1514
1515    #[test]
1516    fn test_uploaded_file_is_valid_with_error() {
1517        let temp = create_temp_file(b"hello", ".txt");
1518        let uploaded = UploadedFile::new(
1519            temp.path(),
1520            "my_file.txt",
1521            Some("text/plain"),
1522            Some(3),
1523            false,
1524        )
1525        .unwrap();
1526        // UPLOAD_ERR_PARTIAL → isValid = false
1527        assert!(!uploaded.is_valid());
1528    }
1529
1530    #[test]
1531    fn test_uploaded_file_is_valid_test_mode() {
1532        // test 模式:error == OK 且文件存在 → is_valid = true
1533        // 对齐 PHP:构造时 check_path = (error === UPLOAD_ERR_OK),所以文件必须存在
1534        let temp = create_temp_file(b"hello", ".txt");
1535        let uploaded = UploadedFile::new(
1536            temp.path(),
1537            "my_file.txt",
1538            Some("text/plain"),
1539            Some(0),
1540            true,
1541        )
1542        .unwrap();
1543        assert!(uploaded.is_valid());
1544    }
1545
1546    #[test]
1547    fn test_uploaded_file_is_valid_test_mode_with_error() {
1548        let uploaded = UploadedFile::new(
1549            "/nonexistent",
1550            "my_file.txt",
1551            Some("text/plain"),
1552            Some(4),
1553            true,
1554        )
1555        .unwrap();
1556        assert!(!uploaded.is_valid());
1557    }
1558
1559    // ------------------------------------------------------------------------
1560    // 组 10:UploadedFile move 测试
1561    // ------------------------------------------------------------------------
1562
1563    #[test]
1564    fn test_uploaded_file_move_test_mode() {
1565        // test 模式:使用 rename(对齐 PHP 第 54 行 parent::move)
1566        let temp = create_temp_file(b"hello", ".txt");
1567        let temp_dir = tempfile::tempdir().unwrap();
1568        let mut uploaded = UploadedFile::new(
1569            temp.path(),
1570            "original.txt",
1571            Some("text/plain"),
1572            Some(0),
1573            true,
1574        )
1575        .unwrap();
1576        let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
1577        assert!(moved.path().is_file());
1578        assert_eq!(moved.basename(), "moved.txt");
1579    }
1580
1581    #[test]
1582    fn test_uploaded_file_move_invalid() {
1583        // 无效上传 → 抛异常(对齐 PHP 第 74 行)
1584        let temp = create_temp_file(b"hello", ".txt");
1585        let temp_dir = tempfile::tempdir().unwrap();
1586        let mut uploaded = UploadedFile::new(
1587            temp.path(),
1588            "original.txt",
1589            Some("text/plain"),
1590            Some(3),
1591            false,
1592        )
1593        .unwrap();
1594        let result = uploaded.move_to(&temp_dir, Some("moved.txt"));
1595        assert!(matches!(result, Err(UploadError::UploadFailed(_))));
1596        // 错误消息对齐 PHP `getErrorMessage()`
1597        if let Err(UploadError::UploadFailed(msg)) = result {
1598            assert_eq!(msg, "only the portion of file is uploaded");
1599        }
1600    }
1601
1602    #[test]
1603    fn test_uploaded_file_move_real() {
1604        // 非 test 模式:move_uploaded_file(Rust 端用 rename)
1605        let temp = create_temp_file(b"hello", ".txt");
1606        let temp_dir = tempfile::tempdir().unwrap();
1607        let mut uploaded = UploadedFile::new(
1608            temp.path(),
1609            "original.txt",
1610            Some("text/plain"),
1611            Some(0),
1612            false,
1613        )
1614        .unwrap();
1615        let moved = uploaded.move_to(&temp_dir, Some("uploaded.txt")).unwrap();
1616        assert!(moved.path().is_file());
1617        assert_eq!(moved.basename(), "uploaded.txt");
1618        // 原文件已移动
1619        assert!(!temp.path().exists());
1620    }
1621
1622    // ------------------------------------------------------------------------
1623    // 组 11:UploadedFile error 测试
1624    // ------------------------------------------------------------------------
1625
1626    #[test]
1627    fn test_uploaded_file_error_message() {
1628        // 对齐 PHP `UploadedFile::getErrorMessage()` 第 82-106 行
1629        let temp = create_temp_file(b"hello", ".txt");
1630
1631        let uploaded_ok = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
1632        assert_eq!(uploaded_ok.error_message(), "unknown upload error");
1633
1634        let uploaded_1 = UploadedFile::new(temp.path(), "f.txt", None, Some(1), false).unwrap();
1635        assert_eq!(
1636            uploaded_1.error_message(),
1637            "upload File size exceeds the maximum value"
1638        );
1639
1640        let uploaded_3 = UploadedFile::new(temp.path(), "f.txt", None, Some(3), false).unwrap();
1641        assert_eq!(
1642            uploaded_3.error_message(),
1643            "only the portion of file is uploaded"
1644        );
1645
1646        let uploaded_4 = UploadedFile::new(temp.path(), "f.txt", None, Some(4), false).unwrap();
1647        assert_eq!(uploaded_4.error_message(), "no file to uploaded");
1648    }
1649
1650    #[test]
1651    fn test_uploaded_file_error_code() {
1652        let temp = create_temp_file(b"hello", ".txt");
1653        let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(7), false).unwrap();
1654        assert_eq!(uploaded.error_code(), UploadErrCode::CantWrite);
1655    }
1656
1657    // ------------------------------------------------------------------------
1658    // 组 12:辅助函数 get_name 测试
1659    // ------------------------------------------------------------------------
1660
1661    #[test]
1662    fn test_get_name_simple() {
1663        // 对齐 PHP `File::getName($name)` 第 146-153 行
1664        assert_eq!(get_name("file.txt").unwrap(), "file.txt");
1665    }
1666
1667    #[test]
1668    fn test_get_name_with_path() {
1669        // 包含 / 的路径 → 返回最后一段
1670        assert_eq!(get_name("/path/to/file.txt").unwrap(), "file.txt");
1671    }
1672
1673    #[test]
1674    fn test_get_name_with_backslash() {
1675        // 对齐 PHP `str_replace('\\', '/', $name)`
1676        assert_eq!(get_name("\\path\\to\\file.txt").unwrap(), "file.txt");
1677    }
1678
1679    #[test]
1680    fn test_get_name_mixed_separators() {
1681        // 混合分隔符
1682        assert_eq!(get_name("\\path/to\\file.txt").unwrap(), "file.txt");
1683    }
1684
1685    #[test]
1686    fn test_get_name_only_filename() {
1687        assert_eq!(get_name("filename").unwrap(), "filename");
1688    }
1689
1690    #[test]
1691    fn test_get_name_rejects_path_traversal() {
1692        // 安全检查:包含 .. 的文件名视为非法(路径遍历攻击防护)
1693        assert!(get_name("../etc/passwd").is_err());
1694        assert!(get_name("file..txt").is_err());
1695        assert!(get_name("..hidden").is_err());
1696    }
1697
1698    // ------------------------------------------------------------------------
1699    // 组 13:PHP 行为对齐测试(R5 硬约束)
1700    // ------------------------------------------------------------------------
1701
1702    #[test]
1703    fn test_php_behavior_hash_name_md5_split() {
1704        // R5-2:hashName('md5') = substr(md5, 0, 2) . '/' . substr(md5, 2)
1705        // "hello" 的 MD5 = 5d41402abc4b2a76b9719d911017c592
1706        // 期望:5d/41402abc4b2a76b9719d911017c592
1707        let temp = create_temp_file(b"hello", "");
1708        let mut file = File::new(temp.path(), true).unwrap();
1709        let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
1710        assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592");
1711    }
1712
1713    #[test]
1714    fn test_php_behavior_hash_name_sha1_split() {
1715        // R5-2:hashName('sha1') = substr(sha1, 0, 2) . '/' . substr(sha1, 2)
1716        // "hello" 的 SHA1 = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
1717        // 期望:aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d
1718        let temp = create_temp_file(b"hello", "");
1719        let mut file = File::new(temp.path(), true).unwrap();
1720        let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
1721        assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1722    }
1723
1724    #[test]
1725    fn test_php_behavior_hash_name_default_format() {
1726        // R5-1:hashName() 默认规则 = date('Ymd')/md5(microtime.pathname).ext
1727        let temp = create_temp_file(b"hello", ".txt");
1728        let mut file = File::new(temp.path(), true).unwrap();
1729        let hash_name = file.hash_name(HashNameRule::Default).unwrap();
1730
1731        // 验证格式:YYYYMMDD/32位hex.txt
1732        let re = regex::Regex::new(r"^\d{8}/[0-9a-f]{32}\.txt$").unwrap();
1733        assert!(
1734            re.is_match(&hash_name),
1735            "hash_name 格式不匹配:{}",
1736            hash_name
1737        );
1738    }
1739
1740    #[test]
1741    fn test_php_behavior_uploaded_file_extension_override() {
1742        // R5-6:UploadedFile::extension() 覆写父类,返回原始扩展名
1743        let temp = create_temp_file(b"hello", ".txt");
1744        let uploaded =
1745            UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
1746                .unwrap();
1747        // extension() 应返回 jpg(原始扩展名)
1748        assert_eq!(uploaded.extension(), "jpg");
1749        // 父类 extension() 返回 txt(路径扩展名)
1750        assert_eq!(uploaded.as_file().extension(), "txt");
1751    }
1752
1753    #[test]
1754    fn test_php_behavior_is_valid_test_mode() {
1755        // R5-3:test 模式只检查 error == OK(构造时仍要求文件存在,因为 check_path = (error === OK))
1756        let temp = create_temp_file(b"hello", ".txt");
1757        let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), true).unwrap();
1758        assert!(uploaded.is_valid());
1759    }
1760
1761    #[test]
1762    fn test_php_behavior_is_valid_non_test_mode_requires_file() {
1763        // R5-3:非 test 模式构造时若 error == OK 则要求文件存在
1764        // 使用存在的临时文件,验证 test=false 时 is_valid 也返回 true(文件存在)
1765        let temp = create_temp_file(b"hello", ".txt");
1766        let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
1767        assert!(uploaded.is_valid());
1768
1769        // 非 test 模式 + 不存在文件 + error == OK → 构造时 FileNotFound 错误
1770        let result = UploadedFile::new("/nonexistent/path", "f.txt", None, Some(0), false);
1771        assert!(matches!(result, Err(UploadError::FileNotFound(_))));
1772    }
1773
1774    #[test]
1775    fn test_php_behavior_error_message_mapping() {
1776        // R5-5:错误码映射
1777        let temp = create_temp_file(b"hello", ".txt");
1778        assert_eq!(
1779            UploadedFile::new(temp.path(), "f", None, Some(1), false)
1780                .unwrap()
1781                .error_message(),
1782            "upload File size exceeds the maximum value"
1783        );
1784        assert_eq!(
1785            UploadedFile::new(temp.path(), "f", None, Some(2), false)
1786                .unwrap()
1787                .error_message(),
1788            "upload File size exceeds the maximum value"
1789        );
1790        assert_eq!(
1791            UploadedFile::new(temp.path(), "f", None, Some(3), false)
1792                .unwrap()
1793                .error_message(),
1794            "only the portion of file is uploaded"
1795        );
1796        assert_eq!(
1797            UploadedFile::new(temp.path(), "f", None, Some(4), false)
1798                .unwrap()
1799                .error_message(),
1800            "no file to uploaded"
1801        );
1802        assert_eq!(
1803            UploadedFile::new(temp.path(), "f", None, Some(6), false)
1804                .unwrap()
1805                .error_message(),
1806            "upload temp dir not found"
1807        );
1808        assert_eq!(
1809            UploadedFile::new(temp.path(), "f", None, Some(7), false)
1810                .unwrap()
1811                .error_message(),
1812            "file write error"
1813        );
1814        assert_eq!(
1815            UploadedFile::new(temp.path(), "f", None, Some(0), false)
1816                .unwrap()
1817                .error_message(),
1818            "unknown upload error"
1819        );
1820    }
1821
1822    #[test]
1823    fn test_php_behavior_move_creates_directory() {
1824        // R5-8:mkdir(directory, 0777, true) 递归创建目录
1825        let temp = create_temp_file(b"hello", ".txt");
1826        let temp_dir = tempfile::tempdir().unwrap();
1827        let nested = temp_dir.path().join("a").join("b").join("c");
1828
1829        let mut file = File::new(temp.path(), true).unwrap();
1830        let moved = file.move_to(&nested, Some("file.txt")).unwrap();
1831
1832        assert!(moved.path().is_file());
1833        assert!(nested.is_dir());
1834    }
1835
1836    #[test]
1837    fn test_php_behavior_move_chmod_unix() {
1838        // R5-8:chmod(target, 0666 & ~umask())
1839        // 仅在 Unix 平台验证
1840        let temp = create_temp_file(b"hello", ".txt");
1841        let temp_dir = tempfile::tempdir().unwrap();
1842
1843        let mut file = File::new(temp.path(), true).unwrap();
1844        let moved = file.move_to(&temp_dir, Some("file.txt")).unwrap();
1845
1846        #[cfg(unix)]
1847        {
1848            use std::os::unix::fs::PermissionsExt;
1849            let perms = std::fs::metadata(moved.path())
1850                .unwrap()
1851                .permissions()
1852                .mode();
1853            // 0666 & ~umask(),umask 通常是 022,所以最终权限是 0644
1854            assert_eq!(perms & 0o777, 0o644);
1855        }
1856        #[cfg(not(unix))]
1857        {
1858            let _ = moved;
1859        }
1860    }
1861
1862    #[test]
1863    fn test_php_behavior_hash_caching() {
1864        // 对齐 PHP 第 56-58 行:hash 缓存
1865        let temp = create_temp_file(b"hello", ".txt");
1866        let mut file = File::new(temp.path(), true).unwrap();
1867        let md5_1 = file.hash(HashAlgo::Md5).unwrap();
1868        // 再次请求相同算法 → 从缓存读取
1869        let md5_2 = file.hash(HashAlgo::Md5).unwrap();
1870        assert_eq!(md5_1, md5_2);
1871    }
1872
1873    #[test]
1874    fn test_php_behavior_hash_name_caching() {
1875        // 对齐 PHP 第 182 行:if (!$this->hashName) 缓存
1876        let temp = create_temp_file(b"hello", ".txt");
1877        let mut file = File::new(temp.path(), true).unwrap();
1878        let name_1 = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
1879        let name_2 = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
1880        // 第二次调用使用缓存,返回第一次的结果(md5 格式)
1881        assert_eq!(name_1, name_2);
1882    }
1883
1884    #[test]
1885    fn test_php_behavior_get_mime_infer() {
1886        // R5-7:getMime 使用 finfo_file(Rust infer crate)
1887        // PNG 文件检测
1888        let png_header = [
1889            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
1890            0x44, 0x52,
1891        ];
1892        let mut file = NamedTempFile::with_suffix(".png").unwrap();
1893        file.write_all(&png_header).unwrap();
1894        let file = File::new(file.path(), true).unwrap();
1895        assert_eq!(file.get_mime().unwrap(), "image/png");
1896    }
1897
1898    #[test]
1899    fn test_php_behavior_uploaded_file_move_test_uses_rename() {
1900        // R5-4:test 模式使用 rename(parent::move)
1901        let temp = create_temp_file(b"hello", ".txt");
1902        let temp_dir = tempfile::tempdir().unwrap();
1903        let mut uploaded =
1904            UploadedFile::new(temp.path(), "original.txt", None, Some(0), true).unwrap();
1905        let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
1906        assert!(moved.path().is_file());
1907        // 原文件已被 rename(不存在)
1908        assert!(!temp.path().exists());
1909    }
1910
1911    #[test]
1912    fn test_php_behavior_uploaded_file_move_non_test_uses_move_uploaded_file() {
1913        // R5-4:非 test 模式使用 move_uploaded_file(Rust 端用 rename)
1914        let temp = create_temp_file(b"hello", ".txt");
1915        let temp_dir = tempfile::tempdir().unwrap();
1916        let mut uploaded =
1917            UploadedFile::new(temp.path(), "original.txt", None, Some(0), false).unwrap();
1918        let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
1919        assert!(moved.path().is_file());
1920        assert!(!temp.path().exists());
1921    }
1922
1923    #[test]
1924    fn test_uploaded_file_as_file_access() {
1925        let temp = create_temp_file(b"hello", ".txt");
1926        let uploaded = UploadedFile::new(
1927            temp.path(),
1928            "original.txt",
1929            Some("text/plain"),
1930            Some(0),
1931            false,
1932        )
1933        .unwrap();
1934        // 不可变访问
1935        assert_eq!(uploaded.as_file().path(), temp.path());
1936        assert_eq!(uploaded.as_file().extension(), "txt");
1937    }
1938
1939    #[test]
1940    fn test_uploaded_file_as_file_mut_access() {
1941        let temp = create_temp_file(b"hello", ".txt");
1942        let mut uploaded = UploadedFile::new(
1943            temp.path(),
1944            "original.txt",
1945            Some("text/plain"),
1946            Some(0),
1947            false,
1948        )
1949        .unwrap();
1950        // 可变访问 — 调用 File 的 hash 方法
1951        let md5 = uploaded.as_file_mut().md5().unwrap();
1952        assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
1953    }
1954}