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() { // 对照:不超阈值时预检不许误伤(否则这条护栏就变成"导出全挂")。