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
@@ -1,5 +1,14 @@
|
||||
//! 读取限额:条目数 / 解压总体积 / 单条目,以及两处封顶实现。
|
||||
//!
|
||||
//! # 两阶段限额(打开 vs 读取)
|
||||
//!
|
||||
//! 打开阶段([`check_limits`])只能拿**中央目录声明的大小**做判断——它会撒谎。
|
||||
//! 因此读取阶段还有第二道:单条目按实际读出字节封顶([`read_capped`]),批量读取
|
||||
//! ([`super::PackArchive::override_files`])再按**累计实际字节**从 [`ReadBudget`] 里扣减。
|
||||
//! 没有预算的批量读取等于把 `max_uncompressed_bytes` 降级成"每条单独算"——一个 2 万条目、
|
||||
//! 每条声明 1 字节却解出几十 MiB 的包能把 CPU/时间耗在这里(r1 p3-4)。
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::io::Read;
|
||||
|
||||
use super::super::error::{PackError, PackResult};
|
||||
@@ -48,6 +57,47 @@ impl Default for PackLimits {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一次批量读取的**累计**预算(读取阶段,按实际读出的字节扣减)。
|
||||
///
|
||||
/// 为什么需要它:中央目录声明的大小可以撒谎,单个条目也被限制在
|
||||
/// [`PackLimits::max_entry_bytes`],但"很多条各自合规"累起来仍可能远超
|
||||
/// `max_uncompressed_bytes`。批量读取(`override_files`)持有一个预算,边读边扣。
|
||||
#[derive(Debug)]
|
||||
pub struct ReadBudget {
|
||||
remaining: Cell<u64>,
|
||||
}
|
||||
|
||||
impl ReadBudget {
|
||||
/// 按总预算新建。
|
||||
#[must_use]
|
||||
pub fn new(limit: u64) -> Self {
|
||||
Self {
|
||||
remaining: Cell::new(limit),
|
||||
}
|
||||
}
|
||||
|
||||
/// 还剩多少字节。
|
||||
#[must_use]
|
||||
pub fn remaining(&self) -> u64 {
|
||||
self.remaining.get()
|
||||
}
|
||||
|
||||
/// 扣减一次实际读出的字节数;超额报 `pack_limits`。
|
||||
///
|
||||
/// 超额时预算**照样扣到 0**(这些字节已经从流里读出来了),后续调用会继续失败——
|
||||
/// 不给"失败一次、下次又能读一点"的机会。
|
||||
pub(super) fn spend(&self, bytes: u64, name: &str) -> PackResult<()> {
|
||||
let remaining = self.remaining.get();
|
||||
self.remaining.set(remaining.saturating_sub(bytes));
|
||||
if bytes > remaining {
|
||||
return Err(PackError::limits(format!(
|
||||
"{name} 之后累计解压超过上限(本次还需 {bytes} 字节,预算已用尽)"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 累计与单条目限额检查(打开阶段)。
|
||||
pub(super) fn check_limits(
|
||||
limits: &PackLimits,
|
||||
@@ -79,9 +129,18 @@ pub(super) fn check_limits(
|
||||
}
|
||||
|
||||
/// 带封顶的读:中央目录里声明的大小可以撒谎,这里按**实际读出的字节**封顶。
|
||||
pub(super) fn read_capped(reader: &mut impl Read, cap: u64, name: &str) -> PackResult<Vec<u8>> {
|
||||
///
|
||||
/// 给了 `budget` 就同时从累计预算里扣(批量读取);单条读取给 `None`,
|
||||
/// 只受 [`PackLimits::max_entry_bytes`] 约束。
|
||||
pub(super) fn read_capped(
|
||||
reader: &mut impl Read,
|
||||
cap: u64,
|
||||
budget: Option<&ReadBudget>,
|
||||
name: &str,
|
||||
) -> PackResult<Vec<u8>> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut buffer = [0_u8; 64 * 1024];
|
||||
let mut total = 0_u64;
|
||||
loop {
|
||||
let read = reader
|
||||
.read(&mut buffer)
|
||||
@@ -89,11 +148,15 @@ pub(super) fn read_capped(reader: &mut impl Read, cap: u64, name: &str) -> PackR
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
if bytes.len() as u64 + read as u64 > cap {
|
||||
total += read as u64;
|
||||
if total > cap {
|
||||
return Err(PackError::limits(format!(
|
||||
"{name} 实际解压超过单条目上限 {cap}"
|
||||
)));
|
||||
}
|
||||
if let Some(budget) = budget {
|
||||
budget.spend(read as u64, name)?;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..read]);
|
||||
}
|
||||
Ok(bytes)
|
||||
|
||||
Reference in new issue
Block a user