fix(core): M110 r1 复审修复:导出安全闸、跨侧重名、第二检用例、半包与失败分级
按 tower 裁定清单逐条改,不扩战场(模型与扩展点未动)。 - p2-1 跨格式导出不再静默丢内容:`PackFormat::check_overrides` 在默认 `write` 写第一个 字节前判定"这份模型的覆盖目录集合能否被本格式表达"。mrpack 要求目录名由 side 决定 (overrides / client-overrides / server-overrides)、CF 只接受单一 Both 侧组;表达不了 即 `pack_unsupported` + hint。`format.rs` 模块文档写死"默认 write 只保证同格式往返", 并列出 M112 必须补的四件事(目录映射、side 合并、加载器 id 规范化、extra 分桶)。 - p2-2 `duplicate_override_paths` 改为按 `path` 跨侧计数,返回 `(路径, 侧集合)`; Both×Client 同路径这条 mrpack 真实冲突现在查得出来。 - p2-3 补 `#[cfg(windows)]` 第二检用例(CON / NUL / COM1.txt / `a.` / 尾空格 / 通配符), 并订正 `archive.rs` 文档里"保留设备名这里不拦"的自相矛盾(实际由 enclosed_name 在 Windows 宿主上拦,第一检放行)。 - p3-1 导出改临时文件 + rename,失败清理临时文件:目标路径不再留半个 zip。 - p3-2 三个 `detect` 不再 `.ok().flatten()` 吞错误:清单在但读不出来 = `pack_metadata`; 注册表汇总时"命中优先于失败",一个都不命中才报,最后才退化成 `pack_unsupported`。 - p3-3 `env_extra`、CF `files[].extra`、`PackLoader::extra` 各补一条读→render→写→再读用例。 - p3-4 新增 `ReadBudget`(Cell<u64>):`override_files` 持有累计预算,按实际读出字节 逐条扣减,声明大小撒谎的包不能靠"很多条各自合规"绕过 `max_uncompressed_bytes`。 - CF 读侧始终产出一个 Both 侧 overrides 组(字段缺省 `overrides`),与 render 对称。 每处修复都有变异证据(改坏 → 对应用例红):p2-1/p2-2/p2-3/p3-1/p3-2/p3-3a-c/p3-4 共 9 条。 门禁:fmt 0 / clippy --workspace --all-targets -D warnings 0 / cargo test --workspace -j 4 exit 0(58 个 test binary,passed 1930,failed 0);全部改动文件 ≤400 行(最大 389)。
This commit is contained in:
1 parent
b34e86bba5
commit
6dd652aced
16 files changed
+804
-98
No files matched your search
@@ -9,18 +9,24 @@
|
||||
//!
|
||||
//! | 威胁 | 做法 |
|
||||
//! |------|------|
|
||||
//! | 路径穿越(`../x`、`/etc/x`、`C:\x`、`a\..\b`、`file:ads`) | 条目名过 [`normalize_file`](第一检),再过 `zip` crate 的 `enclosed_name()`(第二检:它按 **Windows 规则**解析盘符/ADS/保留设备名);任一拒绝即整包拒绝 |
|
||||
//! | 路径穿越(`../x`、`/etc/x`、`C:\x`、`a\..\b`、`file:ads`) | 条目名过 [`normalize_file`](第一检,平台无关的七条规则),再过 `zip` crate 的 `enclosed_name()`(第二检,**按 Windows 规则**解析盘符/ADS/保留设备名);任一拒绝即整包拒绝 |
|
||||
//! | 符号链接条目 | `unix_mode()` 的 `S_IFLNK` 位命中即拒绝(Windows 上无合法用途;解压器会把目标当内容写出来) |
|
||||
//! | 条目数爆炸 | [`PackLimits::max_entries`],打开时按中央目录长度先判 |
|
||||
//! | 解压体积爆炸(zip 炸弹) | [`PackLimits::max_uncompressed_bytes`] 累计 + [`PackLimits::max_entry_bytes`] 单条目;**读取时**再按实际读出的字节数封顶(中央目录里声明的大小可以撒谎) |
|
||||
//! | 同名条目(`./a` 与 `a` 规整后撞车) | 规整后的名字重复即拒绝 |
|
||||
//!
|
||||
//! **为什么第一检不能省**:`enclosed_name()` 允许"深度 0 处的根目录",也就是
|
||||
//! `"/etc/passwd"` 会返回 `Some("/etc/passwd")`——绝对路径必须由我们自己的规则先干掉,
|
||||
//! 库的检查只当跨平台兜底。
|
||||
//! **两条检查各补一条对方没有的规则**(谁也不能省):
|
||||
//!
|
||||
//! **不在这里做的事**:不写盘(那是 M111 导入管线的事),所以 Windows 保留设备名
|
||||
//! (`CON` / `NUL` / `COM1`…)这里不拦——落盘前必须拦,见 `zip::path` 的同名规则。
|
||||
//! - 第一检管**平台无关**的形状:空名、`\0`、`\`、`:`、前导 `/`、任何 `..` 段。
|
||||
//! `enclosed_name()` 在这一点上更松——它允许"深度 0 处的根目录",也就是 `"/etc/passwd"`
|
||||
//! 会返回 `Some("/etc/passwd")`,绝对路径必须由我们先干掉。
|
||||
//! - 第二检管 **Win32 命名空间**:DOS 设备名(`CON` / `NUL` / `AUX` / `COM1..9` / `LPT1..9`)、
|
||||
//! 结尾的点或空格(`a.`、`a `)、`< > " | ? *` 与控制字符。这些第一检全放行,而它们会让
|
||||
//! 落盘时撞到设备、或与另一个名字等价。第二检只在 **Windows 宿主**上生效(`zip` 的
|
||||
//! `cfg(windows)` 分支),所以 `tests/safety.rs` 里对应的用例也是 `#[cfg(windows)]`。
|
||||
//!
|
||||
//! 也就是说:`CON` / `NUL` 这类在 Windows 上**是拦的**(由第二检),非 Windows 宿主上不拦
|
||||
//! (那里它们只是普通文件名,拦了反而拒掉合法包)。
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -43,7 +49,8 @@ mod open;
|
||||
|
||||
pub use entry::PackEntryInfo;
|
||||
pub use limits::{
|
||||
PackLimits, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_UNCOMPRESSED_BYTES,
|
||||
PackLimits, ReadBudget, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_ENTRY_BYTES,
|
||||
DEFAULT_MAX_UNCOMPRESSED_BYTES,
|
||||
};
|
||||
|
||||
use limits::read_capped;
|
||||
@@ -160,7 +167,7 @@ impl PackArchive {
|
||||
let mut reader = archive
|
||||
.by_name(&entry.raw)
|
||||
.map_err(|error| PackError::metadata(format!("解压 {name} 失败:{error}")))?;
|
||||
let bytes = read_capped(&mut reader, self.limits.max_entry_bytes, name)?;
|
||||
let bytes = read_capped(&mut reader, self.limits.max_entry_bytes, None, name)?;
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
}
|
||||
@@ -187,7 +194,15 @@ impl PackArchive {
|
||||
}
|
||||
|
||||
/// 算一个条目的 SHA-1;不存在返回 `Ok(None)`。
|
||||
///
|
||||
/// 单条读取只受 [`PackLimits::max_entry_bytes`] 约束;要跨条目累计限额的批量读取
|
||||
/// 请用 [`Self::override_files`],或自己建 [`ReadBudget`] 交给 [`Self::override_files_with`]。
|
||||
pub fn sha1(&self, name: &str) -> PackResult<Option<String>> {
|
||||
self.sha1_with(name, None)
|
||||
}
|
||||
|
||||
/// 算 SHA-1 并从 `budget` 里扣减实际读出的字节(批量读取用)。
|
||||
fn sha1_with(&self, name: &str, budget: Option<&ReadBudget>) -> PackResult<Option<String>> {
|
||||
let Some(entry) = self.entry(name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -224,6 +239,9 @@ impl PackArchive {
|
||||
self.limits.max_entry_bytes
|
||||
)));
|
||||
}
|
||||
if let Some(budget) = budget {
|
||||
budget.spend(read as u64, name)?;
|
||||
}
|
||||
hasher.update(&buffer[..read]);
|
||||
}
|
||||
Ok(Some(hasher.finish()))
|
||||
@@ -234,7 +252,21 @@ impl PackArchive {
|
||||
/// 收集一个覆盖目录下的文件(相对该目录的路径 + SHA-1 + 字节数),按路径排序。
|
||||
///
|
||||
/// 目录不存在时返回空表——"包没带 overrides"不是错误。
|
||||
///
|
||||
/// 这是**唯一会批量解压的 API**,所以它自己持有一个累计预算
|
||||
/// (`PackLimits::max_uncompressed_bytes`),逐条按实际读出的字节扣减:
|
||||
/// 声明大小撒谎的包不会在这里把 CPU 时间耗光(r1 p3-4)。
|
||||
pub fn override_files(&self, dir: &str) -> PackResult<Vec<OverrideFile>> {
|
||||
let budget = ReadBudget::new(self.limits.max_uncompressed_bytes);
|
||||
self.override_files_with(dir, &budget)
|
||||
}
|
||||
|
||||
/// 同 [`Self::override_files`],但用调用方给的累计预算(M111 想与别处的解压共用额度时用)。
|
||||
pub fn override_files_with(
|
||||
&self,
|
||||
dir: &str,
|
||||
budget: &ReadBudget,
|
||||
) -> PackResult<Vec<OverrideFile>> {
|
||||
let dir = normalize_dir(dir)?;
|
||||
let prefix = format!("{dir}/");
|
||||
let mut collected = Vec::new();
|
||||
@@ -246,7 +278,9 @@ impl PackArchive {
|
||||
if relative.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let sha1 = self.sha1(&entry.name)?.unwrap_or_default();
|
||||
let sha1 = self
|
||||
.sha1_with(&entry.name, Some(budget))?
|
||||
.unwrap_or_default();
|
||||
collected.push(OverrideFile {
|
||||
path: relative.to_string(),
|
||||
sha1,
|
||||
|
||||
Reference in new issue
Block a user