From 9b23221a62d3081ecb8fdd4d21c6b933715ddc70 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 8 Nov 2026 09:07:01 +0800 Subject: [PATCH] =?UTF-8?q?test(m166):=20=E9=92=89=E4=BD=8F=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=20write()=20=E7=9A=84=E9=99=90=E9=A2=9D=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=EF=BC=88=E5=A4=8D=E5=AE=A1=20r1=20p2-1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审 r1 的变异:把 `format.rs` 里 `write()` 传给 `write_with_limits` 的 `WriteLimits::production()` 换成 `WriteLimits::new(u64::MAX, u64::MAX, entries)`, **606 条全绿**。原因是夹具缩到 KiB 之后,"生产路径 + 真超限内容"那种顺带守住生产 限额的证法没了,而 `the_production_limits_are_the_shared_constants` 只钉了 `production()` 的**返回值**、没钉 `write()` 的**调用点**。 补法(不动生产签名、不进发布构建): - 新增 `format/probe.rs`(`#[cfg(test)]`):线程局部的限额记录钩子; `write_with_limits` 进门时 `#[cfg(test)] probe::record(limits);` 记一笔。 - `tests/limits.rs::the_production_write_path_hands_the_shared_constants_to_the_preflight` 走**生产**的 `CURSEFORGE.write`(KiB 级 payload)后断言记到的就是 `WriteLimits::production()` 的逐字段值,并再钉一遍统一常量。 - 反向验:上述变异现在恰好 1 条红(`总量限额:生产路径传错了`,2026-11-08 实测 18446744073709551615 vs 536870912);只把 entry 换成 u64::MAX 也红(`单条目限额`)。 附带(复审非阻断观察): - 观察 2 采纳:新增 `zip64::the_gate_runs_before_any_content_is_opened`,把"闸在读之前" 从结构证明升级成行为证明——源做成**目录**(`metadata()` 有长度、`File::open` 拒绝访问, 本机实测 C:\Windows 20480 字节 / crates/core/src 4096 字节),闸若挪到读之后会拿到 `pack_io` 而不是 `pack_limits`。 - 观察 1 采纳:`tests/limits.rs` 里"改坏常量会在 cargo check 就红"的旧注释改成准确说法 (E0080 只在测试构建出现,裸 `cargo check` 是绿的)。 - `probe.rs` 里 `#![allow(clippy::missing_const_for_thread_local)]`:clippy 1.99 对已经 写成 `const { Cell::new(None) }` 的 `thread_local!` 仍报错(最小复现同样红),是误报; 外挂在宏上的 `#[allow]` 会被判 unused attribute,故用模块级 inner attribute。 cargo test -p unlucky-core --lib:608 passed / 0 failed / 0 ignored(+2 条); clippy --all-targets -D warnings、fmt --check 干净;全仓 0 个 >400 行文件。 --- crates/core/src/addon/pack/format.rs | 10 +++++ crates/core/src/addon/pack/format/probe.rs | 44 ++++++++++++++++++++++ crates/core/src/addon/pack/tests/limits.rs | 42 ++++++++++++++++++++- crates/core/src/addon/pack/tests/zip64.rs | 39 +++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 crates/core/src/addon/pack/format/probe.rs diff --git a/crates/core/src/addon/pack/format.rs b/crates/core/src/addon/pack/format.rs index 220bb27..534c4d8 100644 --- a/crates/core/src/addon/pack/format.rs +++ b/crates/core/src/addon/pack/format.rs @@ -63,6 +63,11 @@ use super::multimc; mod payload; mod write; +/// 测试用的限额记录钩子(`probe.rs`,**只在 `cfg(test)` 里编译**):把"生产 `write()` +/// 到底传了哪份限额"变成可观测的,见该模块文档。 +#[cfg(test)] +pub(in crate::addon::pack) mod probe; + /// 导出时的覆盖内容来源在 [`payload`] 子模块里(`payload.rs`,与本文件拆开是为了守 400 行): /// [`PackPayload::metadata_only`] 只写清单、[`PackPayload::from_root`] 按 /// `//` 取文件、[`PackPayload::with_sources`] 按**包内条目名**查表 @@ -160,6 +165,11 @@ pub(in crate::addon::pack) fn write_with_limits( target: &Path, limits: write::WriteLimits, ) -> PackResult<()> { + // 测试构建里记一笔"这次传的是哪份限额":夹具缩小之后,只有它能把 + // 「生产 `write()` 传的就是 `WriteLimits::production()`」这件事变成可观测的(见 `probe`)。 + #[cfg(test)] + probe::record(limits); + let Some(entry) = format.metadata_entry() else { return Err(PackError::unsupported(format!( "格式 {}({})只支持读取,不支持导出", diff --git a/crates/core/src/addon/pack/format/probe.rs b/crates/core/src/addon/pack/format/probe.rs new file mode 100644 index 0000000..f7e2a3d --- /dev/null +++ b/crates/core/src/addon/pack/format/probe.rs @@ -0,0 +1,44 @@ +//! **只在测试构建里存在**的限额记录钩子(p2-1,复审 r1)。 +//! +//! # 为什么需要它 +//! +//! M166 把夹具从 GiB 缩到 KiB 之后,「生产路径 + 真超限内容」那种顺带守住生产限额的证法没了。 +//! 于是「[`super::PackFormat::write`] 到底把哪份限额交给 [`super::write_with_limits`]」 +//! **没有任何测试能观测**:把它换成 `WriteLimits::new(u64::MAX, u64::MAX, …)` 会全绿 +//! (复审 r1 实测 606 passed)。`the_production_limits_are_the_shared_constants` 只钉了 +//! `production()` 的**返回值**,没钉 `write()` 的**调用点**。 +//! +//! 所以这里加一条捕获缝:`write_with_limits` 进门时记一笔,测试走**生产**的 +//! `CURSEFORGE.write(…)` 之后断言记到的就是 [`super::write::WriteLimits::production`]。 +//! +//! # 三条纪律 +//! +//! 1. **只在 `cfg(test)` 里编译**:发布构建里这个模块不存在,`write_with_limits` 的签名也没动; +//! 2. 记录是**线程局部**的:`cargo test` 每个用例一个线程,并行跑也不会串味; +//! 3. 这是**测试设施**,不是给生产留的"放宽限额"开关——生产路径只有 +//! [`super::write::WriteLimits::production`] 一个来源。 + +// clippy 1.99 的 `missing_const_for_thread_local` 在**已经是** `const { … }` 的写法上仍然报: +// 最小复现 `thread_local! { static A: Cell> = 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 super::write::WriteLimits; + +thread_local! { + /// 本线程最近一次 [`super::write_with_limits`] 收到的限额。 + static LAST_LIMITS: Cell> = const { Cell::new(None) }; +} + +/// 记一笔(由 [`super::write_with_limits`] 进门时调用)。 +pub(super) fn record(limits: WriteLimits) { + LAST_LIMITS.with(|slot| slot.set(Some(limits))); +} + +/// 本线程最近一次记到的限额(`None` = 还没走过 `write_with_limits`)。 +pub(in crate::addon::pack) fn last_limits() -> Option { + LAST_LIMITS.with(Cell::get) +} diff --git a/crates/core/src/addon/pack/tests/limits.rs b/crates/core/src/addon/pack/tests/limits.rs index c2d77e9..ee5d7ae 100644 --- a/crates/core/src/addon/pack/tests/limits.rs +++ b/crates/core/src/addon/pack/tests/limits.rs @@ -120,7 +120,10 @@ fn the_entry_count_gate_is_the_same_number_as_the_read_side() { fn the_three_limits_form_one_chain_and_the_read_side_uses_them() { // ① 关系:单条 ≤ 总量 < ZIP64 ≤ u32。前一半保证"能写出来的包读得回来", // 后一半保证我们永远不需要写 ZIP64,也永远撞不到 mrpack 的 fileSize(u32) 上限。 - // 这三条是**编译期**断言(`const` 块):改坏常量会在 `cargo check` 就红,不用等跑测试。 + // 这三条是**编译期**断言(`const` 块,而且写在 `#[test] fn` 体内 ⇒ **只在测试构建里存在**): + // 改坏常量会在 `cargo test` / `cargo clippy --all-targets` 的编译期就红(E0080), + // 不用等跑到测试函数;但**裸 `cargo check -p unlucky-core`** 是 lib 非测试构建, + // 看不见这些断言(复审 r1 实测:E0080 只在 `--no-run`/`--all-targets` 下出现)。 const _: () = assert!( PACK_MAX_ENTRY_BYTES <= PACK_MAX_UNCOMPRESSED_BYTES, "单条上限必须 ≤ 总量上限(否则单条规则永远够不着)" @@ -251,6 +254,43 @@ fn the_write_side_refuses_a_total_over_the_unified_total_limit() { assert!(!target.exists()); } +#[test] +fn the_production_write_path_hands_the_shared_constants_to_the_preflight() { + // p2-1(复审 r1):夹具缩到 KiB 之后,"生产路径 + 真超限内容"那种顺带守住生产限额的 + // 证法没了,于是「`PackFormat::write` 到底把哪份限额交给 `write_with_limits`」 + // 没有任何测试能观测——把它换成 `WriteLimits::new(u64::MAX, u64::MAX, …)` 会全绿。 + // `the_production_limits_are_the_shared_constants` 只钉了 `production()` 的**返回值**, + // 这条钉的是**调用点**(走生产的 `CURSEFORGE.write`,靠 `format::probe` 记录)。 + let root = TempRoot::new("limits-binding"); + write_file(&root.join("overrides/mods/jei.jar"), b"PK\x03\x04 jei"); + let target = root.join("out.zip"); + + CURSEFORGE + .write( + &manifest_of(vec![override_file("mods/jei.jar")]), + PackPayload::from_root(&root), + &target, + ) + .expect("正常尺寸必须写得出来"); + + let recorded = crate::addon::pack::format::probe::last_limits() + .expect("生产 write() 必须经过 write_with_limits"); + let production = WriteLimits::production(); + assert_eq!(recorded.total, production.total, "总量限额:生产路径传错了"); + assert_eq!( + recorded.entry, production.entry, + "单条目限额:生产路径传错了" + ); + assert_eq!( + recorded.entries, production.entries, + "条目数限额:生产路径传错了" + ); + // 再把生产值本身钉在统一常量上:两条合起来才等于"生产路径用的是读侧那三个数"。 + assert_eq!(recorded.total, PACK_MAX_UNCOMPRESSED_BYTES); + assert_eq!(recorded.entry, PACK_MAX_ENTRY_BYTES); + assert_eq!(recorded.entries, PACK_MAX_ENTRIES); +} + #[test] fn a_real_pack_written_under_the_limit_reopens_under_the_read_limits() { // 端到端的那句话:"我们能写出的包一定能被自己读回来"。用一份正常尺寸的覆盖内容跑一遍。 diff --git a/crates/core/src/addon/pack/tests/zip64.rs b/crates/core/src/addon/pack/tests/zip64.rs index 6d3069d..3b61167 100644 --- a/crates/core/src/addon/pack/tests/zip64.rs +++ b/crates/core/src/addon/pack/tests/zip64.rs @@ -183,6 +183,45 @@ fn a_single_entry_over_the_entry_limit_is_refused_before_a_single_byte_is_read() assert_eq!(leftovers(&root, &["overrides"]), Vec::::new()); } +#[test] +fn the_gate_runs_before_any_content_is_opened() { + // **行为**证明(复审 r1 的观察 2):源做成**目录**——`metadata()` 拿得到长度, + // `File::open` 会失败(Windows NTFS: "拒绝访问",实测 `C:\Windows` = 20480 字节、 + // 仓库里的 `crates/core/src` = 4096 字节)。所以闸若被挪到"读完内容之后",这里拿到的 + // 会是 `pack_io` 而不是 `pack_limits` —— 比只看"临时文件没被创建"更硬。 + let root = TempRoot::new("zip64-before-read"); + let directory = root.join("overrides/mods/not-a-file"); + std::fs::create_dir_all(&directory).expect("建目录"); + let declared = std::fs::metadata(&directory) + .expect("stat 目录(这一步必须成功,否则区分不了两道闸)") + .len(); + if declared == 0 { + // 少数文件系统把目录长度报成 0:这条行为断言在那上面无从构造。此时覆盖仍在 + // (`leftovers` / `!target.exists()` 的结构证明不受影响)。 + return; + } + // 上限比目录的**声明长度**小一字节:总量闸必然先命中,且内容一个字节都不可能读成功。 + let limit = declared - 1; + let target = root.join("out.zip"); + + let error = write_with_limits( + &CURSEFORGE, + &manifest_of(vec![override_file("mods/not-a-file")]), + PackPayload::from_root(&root), + &target, + WriteLimits::new(limit, limit, SMALL_ENTRIES), + ) + .expect_err("声明尺寸超限必须拒"); + + assert_eq!( + error.kind(), + PackErrorKind::Limits, + "闸必须在打开内容**之前**:拿到 pack_io 说明它被挪到读之后了:{error}" + ); + assert!(!target.exists(), "预检失败时目标路径不许出现"); + assert_eq!(leftovers(&root, &["overrides"]), Vec::::new()); +} + #[test] fn an_export_under_the_threshold_still_writes_a_normal_pack() { // 对照:不超阈值时预检不许误伤(否则这条护栏就变成"导出全挂")。