test(m170): 闸顺序用例在 Windows 上也真跑(声明长度自己造 + 反证)

M166 r2 复审的两条「合并后收口」观察:

① `zip64::the_gate_runs_before_any_content_is_opened` 原来量夹具目录的
   `metadata().len()`,量到 0 就 `return`——而本机 NTFS 上空目录就是 0(实测:空 0、
   第 7 个条目起 4096),于是这条「行为证明」在 Windows 上一条断言都没执行。
   现在:先把目录填到长度非零(32 个 filler),拿「声明长度减一字节」当总量上限,
   前提不满足就 `assert!`;单条目上限给 `u64::MAX`,不让 `guard_entry` 当替身。
   另加一条**反证**:同一份夹具把限额放到 `u64::MAX`,必须死在 `File::open`/`read`
   上拿到 `pack_io`——于是上面拿到 `pack_limits` 只能解释为「闸在碰内容之前」。
   同时更正 `one_file_over_the_threshold_...` 里那句过头的注释:`leftovers` /
   `!target.exists()` 在任何失败路径上都成立,属结构侧检,不是「没读一个字节」的证明。

② `probe.rs` 的模块级 `#![allow(clippy::missing_const_for_thread_local)]` 收窄到
   `thread_local!` 内那个 `static` 上(A/B 实测:没带的那组只报一条 lint,带了就干净,
   且不判 unused),模块级 allow 不再替未来的真漏 const 打掩护。

变异验证(本机 Windows/NTFS,跑完已还原):
- 预检挪到 `File::open` 之后 → 本用例红(`left: Io` / `right: Limits`),改前那版全绿;
- 预检改成用 `std::fs::read` 量尺寸 → 同样红;
- 只留结构断言(拆掉 kind/文案/反证)→ 同一变异下**全绿**,证明它们确实区分不了顺序。
This commit is contained in:
AskaEth committed 2026-11-08 09:54:31 +08:00
1 parent 5965575210
commit 72767067f3
2 files changed
+95 -19

No files matched your search

+7 -6
View File
@@ -18,18 +18,19 @@
//! 3. 这是**测试设施**,不是给生产留的"放宽限额"开关——生产路径只有 //! 3. 这是**测试设施**,不是给生产留的"放宽限额"开关——生产路径只有
//! [`super::write::WriteLimits::production`] 一个来源。 //! [`super::write::WriteLimits::production`] 一个来源。
// clippy 1.99 的 `missing_const_for_thread_local` 在**已经是** `const { … }` 的写法上仍然报:
// 最小复现 `thread_local! { static A: Cell<Option<u64>> = const { Cell::new(None) }; }` 一样红
// (`-D warnings` 下直接失败)。是这条 lint 的误报,不是这里漏了 const,所以整模块压掉
// (外挂在 `thread_local!` 上的 `#[allow]` 对展开里的诊断无效,会被报成 unused attribute)。
#![allow(clippy::missing_const_for_thread_local)]
use std::cell::Cell; use std::cell::Cell;
use super::write::WriteLimits; use super::write::WriteLimits;
thread_local! { thread_local! {
/// 本线程最近一次 [`super::write_with_limits`] 收到的限额。 /// 本线程最近一次 [`super::write_with_limits`] 收到的限额。
///
/// `#[allow]` 贴在**这个 `static`** 上(M170):clippy 1.99 的
/// `missing_const_for_thread_local` 在初始化**已经是** `const { … }` 的写法上仍然报误报
/// (最小复现 `static A: Cell<Option<u64>> = const { Cell::new(None) };` 一样红),
/// 所以要压掉。放在这里是最窄的作用域;反过来,外挂在 `thread_local!` **本身**上的
/// `#[allow]` 才是无效的(被宏展开吞掉,报成 unused attribute)。
#[allow(clippy::missing_const_for_thread_local)]
static LAST_LIMITS: Cell<Option<WriteLimits>> = const { Cell::new(None) }; static LAST_LIMITS: Cell<Option<WriteLimits>> = const { Cell::new(None) };
} }
+88 -13
View File
@@ -25,7 +25,16 @@
//! 同一条预检分支(`write::preflight` → `guard_with` / `guard_entry`),一条用例都不 `#[ignore]`、 //! 同一条预检分支(`write::preflight` → `guard_with` / `guard_entry`),一条用例都不 `#[ignore]`、
//! 一条断言都没删。**GiB 级的数值判定**(>2 GiB 与 >4 GiB)搬到了 `format/write/tests.rs`: //! 一条断言都没删。**GiB 级的数值判定**(>2 GiB 与 >4 GiB)搬到了 `format/write/tests.rs`:
//! 那两道闸吃的是 `u64`,本来就不需要落盘。 //! 那两道闸吃的是 `u64`,本来就不需要落盘。
//!
//! # 「声明长度」必须自己造出来(M170)
//!
//! M166 的 `the_gate_runs_before_any_content_is_opened` 把源做成目录、量 `metadata().len()`,
//! 量到 0 就 `return`。而**本机 NTFS 上空目录的长度就是 0**(实测:空目录 0,第 7 个条目起
//! 4096;ext4 空目录本来就报 4096)——所以那条"行为证明"在 Windows 上一条断言都没执行,
//! 只有 CI 的 ubuntu 那侧真跑。现在改成:先把目录填到长度非零,拿**这个长度减一字节**当
//! 总量上限,并且前提不满足时 `assert!` 而不是 `return`——要么真判过东西,要么在这台机器上红。
use std::io::Read;
use std::path::Path; use std::path::Path;
use super::write_file; use super::write_file;
@@ -42,6 +51,11 @@ const SMALL_TOTAL: u64 = 8 * 1024;
const SMALL_ENTRY: u64 = 2 * 1024; const SMALL_ENTRY: u64 = 2 * 1024;
const SMALL_ENTRIES: usize = 16; const SMALL_ENTRIES: usize = 16;
/// 把目录填充到「`metadata().len()` 非零」要几个条目(本机 NTFS 实测第 7 个起报 4096、
/// ext4 空目录就报 4096)。取 32 而不是临界值:不多花一个字节,也不卡在文件系统的条数
/// 阈值上(见 `the_gate_runs_before_any_content_is_opened`)。
const DIR_LENGTH_FILLERS: usize = 32;
/// 上面那三个数组成的限额(`WriteLimits::new` 是 `#[cfg(test)]` 的测试入口)。 /// 上面那三个数组成的限额(`WriteLimits::new` 是 `#[cfg(test)]` 的测试入口)。
fn small_limits() -> WriteLimits { fn small_limits() -> WriteLimits {
WriteLimits::new(SMALL_TOTAL, SMALL_ENTRY, SMALL_ENTRIES) WriteLimits::new(SMALL_TOTAL, SMALL_ENTRY, SMALL_ENTRIES)
@@ -120,8 +134,11 @@ fn one_file_over_the_threshold_is_refused_before_a_single_byte_is_read() {
"要给「把大文件移出实例」这种可执行的提示:{error:?}" "要给「把大文件移出实例」这种可执行的提示:{error:?}"
); );
assert!(!target.exists(), "失败后目标路径不许存在"); assert!(!target.exists(), "失败后目标路径不许存在");
// 临时文件也不许留:`container` 的顺序是 预检 → `File::create(temp)` → 读源文件, // 临时文件也不许留。**但这条不是"一个字节都没读"的证明**(M170 更正):失败分支自己会
// 所以"目录里只有源文件"证明**一个字节的内容都没被打开过**。 // `remove_file(&temp)`,所以它在任何失败路径上都成立,等于"确实返回了 Err"的同义反复。
// 真有牙的判据是上面的 `kind()`:闸若被挪到读内容之后,源文件能读时这里拿到的是
// `pack_io`。顺序本身由 `the_gate_runs_before_any_content_is_opened` 用"读不出来的源"
// 正面证明(含反证)。
assert_eq!(leftovers(&root, &["overrides"]), Vec::<String>::new()); assert_eq!(leftovers(&root, &["overrides"]), Vec::<String>::new());
} }
@@ -185,22 +202,44 @@ fn a_single_entry_over_the_entry_limit_is_refused_before_a_single_byte_is_read()
#[test] #[test]
fn the_gate_runs_before_any_content_is_opened() { fn the_gate_runs_before_any_content_is_opened() {
// **行为**证明(复审 r1 的观察 2):源做成**目录**——`metadata()` 拿得到长度, // **行为**证明(复审 r1 的观察 2):源做成**目录**——`metadata()` 拿得到长度,把它当内容
// `File::open` 会失败(Windows NTFS: "拒绝访问",实测 `C:\Windows` = 20480 字节、 // 读却读不出来(Windows NTFS:`File::open` 直接"拒绝访问";Linux:`open` 成功但 `read`
// 仓库里的 `crates/core/src` = 4096 字节)。所以闸若被挪到"读完内容之后",这里拿到的 // 报 EISDIR)。于是闸若被挪到"读内容"那一段,这里拿到的会是 `pack_io` 而不是
// 会是 `pack_io` 而不是 `pack_limits` —— 比只看"临时文件没被创建"更硬。 // `pack_limits`——比只看"临时文件没被创建"硬,因为后者在任何失败路径上都成立。
//
// 但"硬"的前提是**真有声明长度可判**:见下面的两块 `assert!`(M170)。
let root = TempRoot::new("zip64-before-read"); let root = TempRoot::new("zip64-before-read");
let directory = root.join("overrides/mods/not-a-file"); let directory = root.join("overrides/mods/not-a-file");
std::fs::create_dir_all(&directory).expect("建目录"); std::fs::create_dir_all(&directory).expect("建目录");
// 前提①:长度得**自己造**。NTFS 上空目录报 0(本机实测:空 0、第 7 个条目起 4096),
// 直接量空目录会得到 0 —— r2 版本就是在这里 `return` 的,于是 Windows 上一条断言都没跑。
for index in 0..DIR_LENGTH_FILLERS {
write_file(&directory.join(format!("filler-{index:02}")), b"x");
}
let declared = std::fs::metadata(&directory) let declared = std::fs::metadata(&directory)
.expect("stat 目录(这一步必须成功,否则区分不了两道闸)") .expect("stat 目录(这一步必须成功,否则区分不了两道闸)")
.len(); .len();
if declared == 0 { assert!(
// 少数文件系统把目录长度报成 0:这条行为断言在那上面无从构造。此时覆盖仍在 declared > 0,
// (`leftovers` / `!target.exists()` 的结构证明不受影响)。 "前提:目录的声明长度必须非零才有判据可造;{DIR_LENGTH_FILLERS} 个条目之后仍报 0,\
return; 说明该文件系统不报目录长度,这条用例在那里无法证明闸的顺序(**不许静默 return**)"
} );
// 上限比目录的**声明长度**小一字节:总量闸必然先命中,且内容一个字节都不可能读成功。
// 前提②:这条源**确实**读不出来,否则 `pack_limits` / `pack_io` 区分不了闸的位置。
// 走的就是写循环那一对调用(`File::open` → `read`),不满足照样 `assert!` 而不是跳过。
let consumed = std::fs::File::open(&directory).and_then(|mut file| {
let mut byte = [0_u8; 1];
file.read(&mut byte).map(|_| ())
});
assert!(
consumed.is_err(),
"前提:目录源必须读不出来,否则拿到 pack_limits 也说明不了闸在内容之前:{consumed:?}"
);
// 总量上限 = 目录的**声明长度减一字节**:声明尺寸自己就越线(`guard_with` 是 `>=`),
// 所以拒写的理由是这条覆盖条目的声明尺寸,而不是清单 JSON 顺带越线。
// 单条目上限给 `u64::MAX`:不让 `guard_entry` 当替身——如果总量闸漏算了覆盖条目的声明
// 尺寸,这里就不会拿到 `pack_limits`,而是一路走到读内容上拿到 `pack_io`(用例变红)。
let limit = declared - 1; let limit = declared - 1;
let target = root.join("out.zip"); let target = root.join("out.zip");
@@ -209,7 +248,7 @@ fn the_gate_runs_before_any_content_is_opened() {
&manifest_of(vec![override_file("mods/not-a-file")]), &manifest_of(vec![override_file("mods/not-a-file")]),
PackPayload::from_root(&root), PackPayload::from_root(&root),
&target, &target,
WriteLimits::new(limit, limit, SMALL_ENTRIES), WriteLimits::new(limit, u64::MAX, SMALL_ENTRIES),
) )
.expect_err("声明尺寸超限必须拒"); .expect_err("声明尺寸超限必须拒");
@@ -218,6 +257,42 @@ fn the_gate_runs_before_any_content_is_opened() {
PackErrorKind::Limits, PackErrorKind::Limits,
"闸必须在打开内容**之前**:拿到 pack_io 说明它被挪到读之后了:{error}" "闸必须在打开内容**之前**:拿到 pack_io 说明它被挪到读之后了:{error}"
); );
assert!(
error.message().contains("总量") && error.message().contains(&limit.to_string()),
"拒写理由必须是**总量闸**、且报出在用的上限({limit}):{error}"
);
assert!(
!error.message().contains("读覆盖文件"),
"文案是「读覆盖文件失败」的口径 ⇒ 闸已经在碰内容了:{error}"
);
// # 反证:同一个夹具,闸一放行就必定死在"读内容"上
//
// 这一步是"上面那个 `kind` 断言不是同义反复"的证据:把限额放到远大于声明尺寸,同一个
// 目录源会在 `File::open` / `read` 上炸成 `pack_io`。于是上面拿到 `pack_limits` 只有
// 一种解释——闸在碰内容**之前**就拒了。(而 `!target.exists()` 与"目录里没 `.part`"
// 这两条:失败分支会 `remove_file(temp)`、只有成功分支才 `rename`,所以它们在任何失败
// 路径上都成立,属于**结构侧检**、不是判据。r2 曾把后者当成"一个字节都没读"的证明,
// 那才是同义反复。)
let error = write_with_limits(
&CURSEFORGE,
&manifest_of(vec![override_file("mods/not-a-file")]),
PackPayload::from_root(&root),
&root.join("out-loose.zip"),
WriteLimits::new(u64::MAX, u64::MAX, SMALL_ENTRIES),
)
.expect_err("目录源读不出来:闸放行就只能死在读内容上");
assert_eq!(
error.kind(),
PackErrorKind::Io,
"反证不成立:闸放行后没有死在读内容上,那上面那个 kind 断言就区分不了闸的位置:{error}"
);
assert!(error.message().contains("读覆盖文件"), "{error}");
// 结构侧检(**不是判据**):两次调用到这里都没留下 `out.zip` / `out-loose.zip` / `.part`。
// 这两条在任何失败路径上都成立(失败分支自己 `remove_file(temp)`,只有成功分支才
// `rename`),所以它们只值"确实返回了 Err"的同义反复——上面那条反证把真正有牙的判据
// (`kind()` + 文案)与它们区分开。
assert!(!target.exists(), "预检失败时目标路径不许出现"); assert!(!target.exists(), "预检失败时目标路径不许出现");
assert_eq!(leftovers(&root, &["overrides"]), Vec::<String>::new()); assert_eq!(leftovers(&root, &["overrides"]), Vec::<String>::new());
} }