fix(core): ZIP64 第二道闸不再重复累计预检总量(复审 P2-1)
复审在 tip 54c2d75 上实证的缺陷:`container()` 把 `preflight()` 返回的**全量**声明尺寸 当成写循环累加基数,循环里又对每个文件 `+= bytes.len()`,于是真实总量只要过半阈值 (~2.15 GiB)就被误拒,而且拒在内容读出来之后——"闸在内容进内存之前"的性质同时失效。 两处修: 1. 记账拆成独立类型 `Written`,**从清单 JSON 长度起算**,与预检返回值无关;预检的返回值 现在只给测试与诊断看(`let _ =`,文档写明不许当基数)。 2. 内容改成 1 MiB 缓冲流式写进 zip(`COPY_CHUNK`),不再 `std::fs::read` 整份摊在内存里。 第二道闸看的是"已写字节数",与内存占用解耦;顺带把"拿预检 total 当基数"这类写法 在内存侧也变成无害的小错而不是几个 GiB 的读。 测试(按塔台资源令,默认套件不再真写 2.5 GB): - `write.rs` 新增 `the_container_does_not_double_count_the_preflight_total`:把阈值注入成 几十字节(`container`/`preflight`/`Written` 都收 `limit`,生产路径恒为 `zip::ZIP64_BYTES_THR`),构造"正确实现 70 < 90 通过 / 重复累计 110 ≥ 90 被拒"的窗口, **0.01 秒**钉住 P2-1;实测把 bug 写回去即红。 - `write.rs` 新增 `the_second_gate_counts_from_the_manifest_length_not_the_preflight_total`: 直接对 `Written` 断言 2.5 GB 不拒、撞阈值必拒。 - `zip64.rs` 的 2.5 GB 稀疏文件端到端用例保留但 `#[ignore]`(真跑要 ~70 秒 deflate 与一大片 内存/页面文件,C 盘页面文件已被上一轮撑到 34.8G),实测过一次成功:条目声明尺寸原样 2 500 000 000、产物只有几 MB。跑法写在该用例的文档里。 门禁:fmt 0 / clippy -p unlucky-core --all-targets 0 / cargo test -p unlucky-core 1033 passed 0 failed(lib 480 passed + 1 ignored)。
This commit is contained in:
1 parent
d728ed836f
commit
be280c16a3
3 files changed
+221
-21
No files matched your search
@@ -148,7 +148,15 @@ pub trait PackFormat: Debug + Send + Sync {
|
|||||||
let json = self.render(manifest)?;
|
let json = self.render(manifest)?;
|
||||||
// 临时文件 + rename:中途失败时目标路径上不会留下"看起来能用"的半个 zip(r1 p3-1)。
|
// 临时文件 + rename:中途失败时目标路径上不会留下"看起来能用"的半个 zip(r1 p3-1)。
|
||||||
let temp = write::temporary_path(target);
|
let temp = write::temporary_path(target);
|
||||||
match write::container(self.id(), entry, &json, manifest, payload, &temp) {
|
match write::container(
|
||||||
|
self.id(),
|
||||||
|
entry,
|
||||||
|
&json,
|
||||||
|
manifest,
|
||||||
|
payload,
|
||||||
|
&temp,
|
||||||
|
zip::ZIP64_BYTES_THR,
|
||||||
|
) {
|
||||||
Ok(()) => std::fs::rename(&temp, target).map_err(|error| {
|
Ok(()) => std::fs::rename(&temp, target).map_err(|error| {
|
||||||
let _ = std::fs::remove_file(&temp);
|
let _ = std::fs::remove_file(&temp);
|
||||||
PackError::io(format!(
|
PackError::io(format!(
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
//! (`preflight`),超阈值在任何内容被读进内存**之前**就报 `pack_limits`。
|
//! (`preflight`),超阈值在任何内容被读进内存**之前**就报 `pack_limits`。
|
||||||
//!
|
//!
|
||||||
//! 写循环里那份增量检查保留,当第二道:预检与写入之间文件可能变大(实例目录不是快照)。
|
//! 写循环里那份增量检查保留,当第二道:预检与写入之间文件可能变大(实例目录不是快照)。
|
||||||
|
//! **第二道自己从清单长度起算**,绝不拿预检的返回值当基数——两者合起来会重复累计,
|
||||||
|
//! 于是真实总量只要过半阈值(~2.15 GiB)就被误拒,而且拒在内容已经读出来之后(P2-1)。
|
||||||
|
//!
|
||||||
|
//! # 内容按块搬运,不整份读进内存
|
||||||
|
//!
|
||||||
|
//! 覆盖文件用 1 MiB 缓冲流式写进 zip(`COPY_CHUNK`)。这不是优化癖:`std::fs::read` 会把
|
||||||
|
//! 一份 2.5 GiB 的 mod 整份摊在内存里,恰恰破坏"闸在内容进内存之前"这条性质——即使判据
|
||||||
|
//! 后来修对了,误拒也发生在把几个 GiB 读进内存之后。流式之后第二道闸看的是**已写字节数**,
|
||||||
|
//! 与内存占用无关。
|
||||||
//!
|
//!
|
||||||
//! # 阈值与声明口径
|
//! # 阈值与声明口径
|
||||||
//!
|
//!
|
||||||
@@ -19,7 +28,7 @@
|
|||||||
//! 官方实现同样按这个粗粒度判。
|
//! 官方实现同样按这个粗粒度判。
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::super::error::{PackError, PackResult};
|
use super::super::error::{PackError, PackResult};
|
||||||
@@ -27,6 +36,9 @@ use super::super::manifest::PackManifest;
|
|||||||
use super::super::path::{normalize_dir, normalize_file};
|
use super::super::path::{normalize_dir, normalize_file};
|
||||||
use super::payload::PackPayload;
|
use super::payload::PackPayload;
|
||||||
|
|
||||||
|
/// 搬覆盖内容时的缓冲大小(流式,不把文件整份读进内存)。
|
||||||
|
const COPY_CHUNK: usize = 1024 * 1024;
|
||||||
|
|
||||||
/// 与目标同目录的临时文件路径(同卷 `rename` 才能原子替换)。
|
/// 与目标同目录的临时文件路径(同卷 `rename` 才能原子替换)。
|
||||||
pub(super) fn temporary_path(target: &Path) -> PathBuf {
|
pub(super) fn temporary_path(target: &Path) -> PathBuf {
|
||||||
let mut name = target.file_name().map_or_else(
|
let mut name = target.file_name().map_or_else(
|
||||||
@@ -45,9 +57,12 @@ pub(super) fn container(
|
|||||||
manifest: &PackManifest,
|
manifest: &PackManifest,
|
||||||
payload: PackPayload<'_>,
|
payload: PackPayload<'_>,
|
||||||
temp: &Path,
|
temp: &Path,
|
||||||
|
limit: u64,
|
||||||
) -> PackResult<()> {
|
) -> PackResult<()> {
|
||||||
// 先判总量再碰磁盘:超限时临时文件都不建,更不会读进一份 4 GiB 的内容。
|
// 先判总量再碰磁盘:超限时临时文件都不建,更不会读进一份 4 GiB 的内容。
|
||||||
let mut total = preflight(json.len() as u64, manifest, payload)?;
|
// **返回值只属于预检**:写循环有自己的计数器(`Written`),拿它当基数会把全量声明尺寸
|
||||||
|
// 再累加一遍 ⇒ 过拒窗口(P2-1)。
|
||||||
|
let _ = preflight(json.len() as u64, manifest, payload, limit)?;
|
||||||
|
|
||||||
let file = File::create(temp)
|
let file = File::create(temp)
|
||||||
.map_err(|error| PackError::io(format!("创建 {} 失败:{error}", temp.display())))?;
|
.map_err(|error| PackError::io(format!("创建 {} 失败:{error}", temp.display())))?;
|
||||||
@@ -62,23 +77,33 @@ pub(super) fn container(
|
|||||||
.write_all(json.as_bytes())
|
.write_all(json.as_bytes())
|
||||||
.map_err(|error| PackError::io(format!("写 {entry} 失败:{error}")))?;
|
.map_err(|error| PackError::io(format!("写 {entry} 失败:{error}")))?;
|
||||||
|
|
||||||
|
// 第二道闸的计数器:**从清单长度重新起算**,与预检的结果无关(见 `Written`)。
|
||||||
|
let mut written = Written::new(json.len() as u64, limit);
|
||||||
|
let mut buffer = vec![0_u8; COPY_CHUNK];
|
||||||
for group in &manifest.overrides {
|
for group in &manifest.overrides {
|
||||||
let dir = normalize_dir(&group.dir)?;
|
let dir = normalize_dir(&group.dir)?;
|
||||||
for override_file in &group.files {
|
for override_file in &group.files {
|
||||||
let path = normalize_file(&override_file.path)?;
|
let path = normalize_file(&override_file.path)?;
|
||||||
let name = format!("{dir}/{path}");
|
let name = format!("{dir}/{path}");
|
||||||
let source = source_for(payload, &dir, &path)?;
|
let source = source_for(payload, &dir, &path)?;
|
||||||
let bytes = std::fs::read(&source).map_err(|error| {
|
let mut source_file = File::open(&source).map_err(|error| {
|
||||||
PackError::io(format!("读覆盖文件 {} 失败:{error}", source.display()))
|
PackError::io(format!("读覆盖文件 {} 失败:{error}", source.display()))
|
||||||
})?;
|
})?;
|
||||||
total += bytes.len() as u64;
|
|
||||||
guard(total)?;
|
|
||||||
writer
|
writer
|
||||||
.start_file(name.clone(), options)
|
.start_file(name.clone(), options)
|
||||||
.map_err(|error| PackError::io(format!("写 {name} 失败:{error}")))?;
|
.map_err(|error| PackError::io(format!("写 {name} 失败:{error}")))?;
|
||||||
writer
|
loop {
|
||||||
.write_all(&bytes)
|
let read = source_file.read(&mut buffer).map_err(|error| {
|
||||||
.map_err(|error| PackError::io(format!("写 {name} 失败:{error}")))?;
|
PackError::io(format!("读覆盖文件 {} 失败:{error}", source.display()))
|
||||||
|
})?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
writer
|
||||||
|
.write_all(&buffer[..read])
|
||||||
|
.map_err(|error| PackError::io(format!("写 {name} 失败:{error}")))?;
|
||||||
|
written.spend(read as u64)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer
|
writer
|
||||||
@@ -89,10 +114,17 @@ pub(super) fn container(
|
|||||||
|
|
||||||
/// 写盘前的**声明尺寸预检**:只量 `metadata().len()`,不读内容。
|
/// 写盘前的**声明尺寸预检**:只量 `metadata().len()`,不读内容。
|
||||||
///
|
///
|
||||||
/// 返回清单 + 全部覆盖文件的声明总量(供写循环接着累加当第二道)。
|
/// 返回值是"清单 + 全部覆盖文件的声明总量",**只给测试与诊断看**。写循环有自己的计数器
|
||||||
fn preflight(json_len: u64, manifest: &PackManifest, payload: PackPayload<'_>) -> PackResult<u64> {
|
/// (从清单长度起算)——曾经把它直接当累加基数,结果每个文件的字节被算两遍,真实总量过半
|
||||||
|
/// 阈值(~2.15 GiB)就被误拒(P2-1,已有 2.5 GB 的端到端用例钉住这个窗口)。
|
||||||
|
fn preflight(
|
||||||
|
json_len: u64,
|
||||||
|
manifest: &PackManifest,
|
||||||
|
payload: PackPayload<'_>,
|
||||||
|
limit: u64,
|
||||||
|
) -> PackResult<u64> {
|
||||||
let mut total = json_len;
|
let mut total = json_len;
|
||||||
guard(total)?;
|
guard_with(total, limit)?;
|
||||||
for group in &manifest.overrides {
|
for group in &manifest.overrides {
|
||||||
let dir = normalize_dir(&group.dir)?;
|
let dir = normalize_dir(&group.dir)?;
|
||||||
for override_file in &group.files {
|
for override_file in &group.files {
|
||||||
@@ -106,7 +138,7 @@ fn preflight(json_len: u64, manifest: &PackManifest, payload: PackPayload<'_>) -
|
|||||||
})?
|
})?
|
||||||
.len();
|
.len();
|
||||||
total += length;
|
total += length;
|
||||||
guard(total)?;
|
guard_with(total, limit)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(total)
|
Ok(total)
|
||||||
@@ -126,15 +158,37 @@ fn source_for(payload: PackPayload<'_>, dir: &str, path: &str) -> PackResult<Pat
|
|||||||
Ok(root.join(dir).join(path))
|
Ok(root.join(dir).join(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ZIP64 阈值闸(`pack_limits`)。
|
/// 写循环的第二道闸(**已写出**的未压缩字节数)。
|
||||||
fn guard(total: u64) -> PackResult<()> {
|
///
|
||||||
guard_with(total, zip::ZIP64_BYTES_THR)
|
/// 起点是**清单 JSON 的长度**,与 `preflight` 的返回值无关:两者混用会把每个文件的字节算
|
||||||
|
/// 两遍,真实总量过半阈值(~2.15 GiB)就被误拒(P2-1)。类型上也没法混用——`Written`
|
||||||
|
/// 由 `u64` 构造,而预检的返回值是"全量声明尺寸",只有显式取值才拿得到。
|
||||||
|
struct Written {
|
||||||
|
total: u64,
|
||||||
|
limit: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 判据本体:`total` **达到**阈值就拒(`>=` 不是 `>`,官方实现也是"撞上就不写")。
|
impl Written {
|
||||||
|
const fn new(json_len: u64, limit: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
total: json_len,
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记一笔实际写出的字节,并在**每一块之后**过闸(大文件因此能在中途被拦下)。
|
||||||
|
fn spend(&mut self, bytes: u64) -> PackResult<()> {
|
||||||
|
self.total = self.total.saturating_add(bytes);
|
||||||
|
guard_with(self.total, self.limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ZIP64 阈值闸(`pack_limits`):`total` **达到**阈值就拒(`>=` 不是 `>`,官方实现也是
|
||||||
|
/// "撞上就不写")。
|
||||||
///
|
///
|
||||||
/// 抽出 `limit` 参数只为了能在小数值上钉住这条比较——端到端用例必须造 4 GiB 的声明尺寸,
|
/// 生产路径的 `limit` 恒为 `zip::ZIP64_BYTES_THR`(由 `format.rs` 传下来);把它做成参数是为了
|
||||||
/// 而"`>=` 还是 `>`"这种一位之差在小数值上就能验。
|
/// 能用几十字节的小数值钉住"边界在 `>=` 上"和"写循环不许重复累计"这两件事——真造 4 GiB 的
|
||||||
|
/// 包要几十秒 CPU 与一大片内存/页面文件,本机资源纪律下不经默认套件跑(塔台 2026-11-07)。
|
||||||
fn guard_with(total: u64, limit: u64) -> PackResult<()> {
|
fn guard_with(total: u64, limit: u64) -> PackResult<()> {
|
||||||
if total >= limit {
|
if total >= limit {
|
||||||
return Err(PackError::limits(format!(
|
return Err(PackError::limits(format!(
|
||||||
@@ -163,6 +217,88 @@ mod tests {
|
|||||||
assert!(guard_with(101, 100).is_err());
|
assert!(guard_with(101, 100).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_second_gate_counts_from_the_manifest_length_not_the_preflight_total() {
|
||||||
|
// P2-1 的窗口:真实总量在 [阈值一半, 阈值) 之间的包**不许**被拒。这一条不真写
|
||||||
|
// 2.5 GB(那要 ~70 秒 deflate 与一大片内存/页面文件),而是直接把窗口里的字节数
|
||||||
|
// 喂给第二道闸的计数器——它的起点是清单长度,所以只有"把预检的全量 total 当基数"
|
||||||
|
// 那种写法才会在这里红(红法是 `spend` 返回 Limits)。
|
||||||
|
let half = zip::ZIP64_BYTES_THR / 2;
|
||||||
|
let mut written = Written::new(28, zip::ZIP64_BYTES_THR);
|
||||||
|
written
|
||||||
|
.spend(2_500_000_000)
|
||||||
|
.expect("2.5 GB 没到 4 GiB,第二道闸不许拒");
|
||||||
|
assert!(2_500_000_000 > half, "样本要落在过拒窗口里");
|
||||||
|
|
||||||
|
// 对照:撞上阈值必须拒,且正好在阈值处(不是超一点点才拒)。
|
||||||
|
let mut written = Written::new(28, zip::ZIP64_BYTES_THR);
|
||||||
|
let error = written
|
||||||
|
.spend(zip::ZIP64_BYTES_THR)
|
||||||
|
.expect_err("撞阈值必须拒");
|
||||||
|
assert_eq!(
|
||||||
|
error.kind(),
|
||||||
|
super::super::super::error::PackErrorKind::Limits
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_container_does_not_double_count_the_preflight_total() {
|
||||||
|
// 容器层的窗口回归钉(P2-1),**用几十字节的 limit 把窗口缩小到可测尺寸**:
|
||||||
|
// `json` 30 字节 + 覆盖文件 40 字节 = 声明总量 70。
|
||||||
|
// - 正确实现:第二道闸从 30 起算,写到 70 也不到 100 ⇒ 导出成功;
|
||||||
|
// - P2-1 的写法(把预检返回的全量 70 当基数):写循环再累加 40 ⇒ 110 ≥ 100 ⇒ 误拒。
|
||||||
|
// 真造 2.5 GB 才能暴露同一个 bug,本机资源纪律下不跑(那条留成 `zip64.rs` 里的
|
||||||
|
// `#[ignore]` 用例)。
|
||||||
|
let root = std::env::temp_dir().join(format!("ull-double-count-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(root.join("overrides/mods")).expect("建目录");
|
||||||
|
std::fs::write(root.join("overrides/mods/a.bin"), [0_u8; 40]).expect("写覆盖文件");
|
||||||
|
// 清单很短,limit 取"声明总量 + 20 字节"的余量:正确实现只写到 70,重复累计会写到 110。
|
||||||
|
let json = "{\"name\":\"0123456789012345\"}";
|
||||||
|
let declared = json.len() as u64 + 40;
|
||||||
|
let limit = declared + 20;
|
||||||
|
let mut manifest = PackManifest::new("double count");
|
||||||
|
manifest.overrides.push(PackOverrides {
|
||||||
|
dir: "overrides".to_string(),
|
||||||
|
side: PackSide::Both,
|
||||||
|
files: vec![OverrideFile {
|
||||||
|
path: "mods/a.bin".to_string(),
|
||||||
|
sha1: String::new(),
|
||||||
|
size: 40,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
let target = root.join("out.zip");
|
||||||
|
|
||||||
|
container(
|
||||||
|
"curseforge",
|
||||||
|
"manifest.json",
|
||||||
|
json,
|
||||||
|
&manifest,
|
||||||
|
PackPayload::from_root(&root),
|
||||||
|
&target,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
.expect("声明总量没到 limit:写循环不许把预检的 total 重复累计");
|
||||||
|
|
||||||
|
// 对照:limit 降到声明总量本身 ⇒ 预检就该拒,且临时文件都没建。
|
||||||
|
let refused = root.join("refused.zip");
|
||||||
|
let error = container(
|
||||||
|
"curseforge",
|
||||||
|
"manifest.json",
|
||||||
|
json,
|
||||||
|
&manifest,
|
||||||
|
PackPayload::from_root(&root),
|
||||||
|
&refused,
|
||||||
|
declared,
|
||||||
|
)
|
||||||
|
.expect_err("声明总量达到 limit 必须拒");
|
||||||
|
assert_eq!(
|
||||||
|
error.kind(),
|
||||||
|
super::super::super::error::PackErrorKind::Limits
|
||||||
|
);
|
||||||
|
assert!(!refused.exists(), "预检失败时目标路径不许出现");
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_preflight_measures_declared_sizes_not_content() {
|
fn the_preflight_measures_declared_sizes_not_content() {
|
||||||
// 预检必须只量 `metadata().len()`:这一条用"小文件 + 3 GiB 稀疏文件"钉住,
|
// 预检必须只量 `metadata().len()`:这一条用"小文件 + 3 GiB 稀疏文件"钉住,
|
||||||
@@ -193,8 +329,13 @@ mod tests {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let total = preflight(7, &manifest, PackPayload::from_root(&root))
|
let total = preflight(
|
||||||
.expect("3 GiB 总量仍在阈值之下,预检不许误伤");
|
7,
|
||||||
|
&manifest,
|
||||||
|
PackPayload::from_root(&root),
|
||||||
|
zip::ZIP64_BYTES_THR,
|
||||||
|
)
|
||||||
|
.expect("3 GiB 总量仍在阈值之下,预检不许误伤");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
total,
|
total,
|
||||||
7 + 10 + 3 * 1024 * 1024 * 1024,
|
7 + 10 + 3 * 1024 * 1024 * 1024,
|
||||||
|
|||||||
@@ -129,3 +129,54 @@ fn an_export_under_the_threshold_still_writes_a_normal_pack() {
|
|||||||
let written = crate::addon::pack::PackArchive::open_default(&target).expect("打开导出的包");
|
let written = crate::addon::pack::PackArchive::open_default(&target).expect("打开导出的包");
|
||||||
assert!(written.has_entry("overrides/mods/jei.jar"));
|
assert!(written.has_entry("overrides/mods/jei.jar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 端到端版的窗口回归钉(**不进默认套件**:真写 2.5 GB 要 ~70 秒 deflate,且会把机器内存 /
|
||||||
|
/// 页面文件顶上去;塔台 2026-11-07 的资源配置令要求避免)。
|
||||||
|
///
|
||||||
|
/// 默认套件里由 `format::write` 的单测 `the_second_gate_counts_from_the_manifest_length_not_
|
||||||
|
/// the_preflight_total` 便宜地钉住同一个窗口(把窗口里的字节数直接喂给第二道闸的计数器)。
|
||||||
|
/// 本条留给"真跑一遍确认容器层接线没错"的场合:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// cargo test -p unlucky-core --lib -- zip64 --ignored --nocapture
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// 交付时的实测(M152 r2):单文件 2 500 000 000 字节导出**成功**,包内条目声明尺寸原样为
|
||||||
|
/// 2.5 GB,耗时 ~70 秒、源文件稀疏(占盘 0),产物是压缩后的零(几 MB)。
|
||||||
|
#[test]
|
||||||
|
#[ignore = "真写 2.5 GB(~70 秒 deflate + 一片内存):默认套件用 Written 的便宜单测钉同一窗口"]
|
||||||
|
fn a_single_file_inside_the_double_count_window_still_exports() {
|
||||||
|
// P2-1 的回归钉:预检返回的是**全量**声明尺寸,写循环曾经直接拿它当累加基数,于是每个
|
||||||
|
// 文件的字节被算两遍 —— 真实总量只要过半阈值(≈2.15 GiB)就在内容读完**之后**被误拒。
|
||||||
|
// 2.5 GB 正落在这个窗口里:它必须能导出,且中途不许把整个人内容摊进内存。
|
||||||
|
let root = temp_root("zip64-window");
|
||||||
|
let length = 2_500_000_000_u64;
|
||||||
|
assert!(length < zip::ZIP64_BYTES_THR, "样本必须在阈值之下");
|
||||||
|
assert!(
|
||||||
|
length > zip::ZIP64_BYTES_THR / 2,
|
||||||
|
"样本必须落在过拒窗口里(≥ 阈值一半)"
|
||||||
|
);
|
||||||
|
sparse_file(&root.join("overrides/mods/big.jar"), length);
|
||||||
|
let manifest = manifest_of(vec![override_file("mods/big.jar")]);
|
||||||
|
let target = root.join("out.zip");
|
||||||
|
|
||||||
|
let result = CURSEFORGE.write(&manifest, PackPayload::from_root(&root), &target);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"2.5 GB 的覆盖文件没超 ZIP64 阈值,不许误拒:{:?}",
|
||||||
|
result.err()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 包内条目的**声明未压缩尺寸**就是 2.5 GB(真写出来的是压缩后的零,只有几 MB)。
|
||||||
|
// 这里直接用 `zip` 的读者,而不是 `PackArchive::open_default`:后者对**不可信输入**有
|
||||||
|
// 单条目 64 MiB 的上限(`PackLimits`),拿它读我们自己刚写的包会撞限额,与本次判据无关。
|
||||||
|
let file = std::fs::File::open(&target).expect("打开导出包");
|
||||||
|
let mut archive = zip::ZipArchive::new(file).expect("读 zip");
|
||||||
|
let entry = archive
|
||||||
|
.by_name("overrides/mods/big.jar")
|
||||||
|
.expect("覆盖条目要在包里");
|
||||||
|
assert_eq!(entry.size(), length, "声明尺寸要原样带过去");
|
||||||
|
|
||||||
|
// 稀疏源文件只占元数据,但产物与目录都清掉,别让 temp 越积越多。
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user