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