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
@@ -136,7 +136,7 @@ fn multimc_needs_a_non_empty_component_graph() {
|
||||
);
|
||||
let pack = PackArchive::open_default(&target).expect("打开包");
|
||||
assert_eq!(
|
||||
MULTIMC.detect(&pack),
|
||||
MULTIMC.detect(&pack).expect("mmc-pack.json 可读"),
|
||||
None,
|
||||
"空 components 是空壳,不能当 MultiMC 实例"
|
||||
);
|
||||
@@ -159,3 +159,65 @@ fn multimc_instance_directory_is_detected() {
|
||||
assert_eq!(matched.format().id(), "multimc");
|
||||
assert!(!matched.is_ambiguous());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_corrupt_manifest_is_a_metadata_error_not_unsupported() {
|
||||
// r1 p3-2:清单在、但读不出来 = 包坏了(`pack_metadata`)。之前三个 detect 都用
|
||||
// `.ok().flatten()` 吞掉解析错误,结果被报成「没有格式认领」——M111 的失败分级会据此
|
||||
// 把"可以重下文件"的包判成"不支持"。
|
||||
let target = temp_root("corrupt-manifest").join("bad.zip");
|
||||
write_zip(&target, &[("manifest.json", &br#"{"name": "#[..])]);
|
||||
let pack = PackArchive::open_default(&target).expect("zip 本身可读");
|
||||
|
||||
let error = PackRegistry::builtin()
|
||||
.detect(&pack)
|
||||
.expect_err("坏清单必须报错");
|
||||
assert_eq!(error.kind(), PackErrorKind::Metadata, "{error}");
|
||||
assert!(
|
||||
!error.message().contains("没有格式认领"),
|
||||
"不许把「包坏了」说成「不认识」:{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_corrupt_foreign_manifest_does_not_hide_a_valid_mrpack() {
|
||||
// 命中优先于失败:包里躺着一份别人的坏 manifest.json,不该让真 mrpack 也变得不可用。
|
||||
let target = temp_root("corrupt-foreign").join("mixed.zip");
|
||||
write_zip(
|
||||
&target,
|
||||
&[
|
||||
("manifest.json", &br#"{"name": "#[..]),
|
||||
("modrinth.index.json", MR_INDEX),
|
||||
],
|
||||
);
|
||||
let pack = PackArchive::open_default(&target).expect("zip 本身可读");
|
||||
let matched = PackRegistry::builtin()
|
||||
.detect(&pack)
|
||||
.expect("mrpack 命中应当优先于其它格式的坏文件");
|
||||
assert_eq!(matched.format().id(), "modrinth");
|
||||
assert!(matched.also_matched().is_empty(), "CF 没命中(它坏了)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_manifest_is_not_an_error() {
|
||||
// 反向对照:**没有** `manifest.json` 是"不是 CF",不是"包坏了"。
|
||||
let target = temp_root("no-manifest").join("mmc.zip");
|
||||
write_zip(
|
||||
&target,
|
||||
&[(
|
||||
"mmc-pack.json",
|
||||
&br#"{"formatVersion":1,"components":[{"uid":"net.minecraft","version":"1.20.1"}]}"#[..],
|
||||
)],
|
||||
);
|
||||
let pack = PackArchive::open_default(&target).expect("zip 本身可读");
|
||||
assert_eq!(CURSEFORGE.detect(&pack).expect("缺清单不是错误"), None);
|
||||
assert_eq!(
|
||||
PackRegistry::builtin()
|
||||
.detect(&pack)
|
||||
.expect("MultiMC 应当命中")
|
||||
.format()
|
||||
.id(),
|
||||
"multimc"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! 导出面的安全闸(r1 p2-1 / p3-1):**跨格式导出必须被拒,失败不留半个包**。
|
||||
//!
|
||||
//! 这一组不测"映射是否做对"(那是 M112),只测"表达不了的时候会不会静默产出空包"。
|
||||
|
||||
use super::{fixtures, temp_root};
|
||||
use crate::addon::pack::archive::PackArchive;
|
||||
use crate::addon::pack::curseforge::CURSEFORGE;
|
||||
use crate::addon::pack::error::PackErrorKind;
|
||||
use crate::addon::pack::format::{PackFormat, PackPayload};
|
||||
use crate::addon::pack::manifest::{OverrideFile, PackManifest, PackOverrides, PackSide};
|
||||
use crate::addon::pack::modrinth::MODRINTH;
|
||||
use crate::addon::pack::multimc::MULTIMC;
|
||||
|
||||
#[test]
|
||||
fn a_multimc_instance_cannot_be_written_as_mrpack() {
|
||||
// r1 p2-1 实测过的那条:MultiMC 模型写 mrpack 会返回 Ok,但覆盖内容落在 `.minecraft/...`,
|
||||
// mrpack 导入器只解压 overrides / client-overrides / server-overrides ⇒ mods 数量为 0。
|
||||
// 现在必须在写第一个字节之前就拒。
|
||||
let root = fixtures::multimc("export-mmc-mr");
|
||||
let pack = PackArchive::open_default(&root).expect("打开实例目录");
|
||||
let mut manifest = MULTIMC.read(&pack).expect("读 MultiMC 清单");
|
||||
manifest.version = Some("1.0.0".to_string()); // mrpack 需要 versionId,别让别的错误顶包
|
||||
|
||||
let target = root.with_file_name("cross.mrpack");
|
||||
let error = MODRINTH
|
||||
.write(&manifest, PackPayload::from_root(&root), &target)
|
||||
.expect_err("跨格式导出必须被拒,不能产出 mods 为 0 的包");
|
||||
assert_eq!(error.kind(), PackErrorKind::Unsupported, "{error}");
|
||||
assert!(
|
||||
error.message().contains(".minecraft"),
|
||||
"错误要说清是哪个目录表达不了:{}",
|
||||
error.message()
|
||||
);
|
||||
assert!(error.hint().is_some(), "要给下一步提示(目录映射属 M112)");
|
||||
assert!(!target.exists(), "被拒时不许留下目标文件");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_mrpack_with_split_sides_cannot_be_written_as_curseforge() {
|
||||
// CF 只有一个 `overrides` 目录字段,表达不了 client/server 分侧;现在是拒,不是猜。
|
||||
let fixture = fixtures::modrinth("export-mr-cf");
|
||||
let pack = PackArchive::open_default(&fixture.zip).expect("打开 mrpack 夹具");
|
||||
let manifest = MODRINTH.read(&pack).expect("读 mrpack 清单");
|
||||
|
||||
let target = fixture.zip.with_file_name("cross.zip");
|
||||
let error = CURSEFORGE
|
||||
.write(&manifest, PackPayload::from_root(&fixture.dir), &target)
|
||||
.expect_err("CF 表达不了分侧覆盖目录");
|
||||
assert_eq!(error.kind(), PackErrorKind::Unsupported, "{error}");
|
||||
assert!(
|
||||
error.message().contains("client"),
|
||||
"错误要指出被拒的是哪一侧:{}",
|
||||
error.message()
|
||||
);
|
||||
assert!(!target.exists(), "被拒时不许留下目标文件");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curseforge_refuses_more_than_one_both_side_group() {
|
||||
// CF 的 `overrides` 只有一个字段:两个 Both 组必然有一个不被清单声明(静默丢内容)。
|
||||
let mut manifest = PackManifest::new("两个 Both 组");
|
||||
manifest.minecraft = Some("1.20.1".to_string());
|
||||
manifest
|
||||
.overrides
|
||||
.push(PackOverrides::new("overrides", PackSide::Both));
|
||||
manifest
|
||||
.overrides
|
||||
.push(PackOverrides::new("extra-overrides", PackSide::Both));
|
||||
|
||||
let target = temp_root("export-cf-two").join("out.zip");
|
||||
let error = CURSEFORGE
|
||||
.write(&manifest, PackPayload::metadata_only(), &target)
|
||||
.expect_err("多出来的 Both 组会被清单漏掉,必须拒");
|
||||
assert_eq!(error.kind(), PackErrorKind::Unsupported, "{error}");
|
||||
assert!(!target.exists(), "被拒时不许留下目标文件");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_export_leaves_neither_a_half_pack_nor_a_temp_file() {
|
||||
// r1 p3-1:清单写完了、覆盖文件读不出来 → 目标路径与临时文件都不许留。
|
||||
let mut manifest = PackManifest::new("缺覆盖文件的模型");
|
||||
manifest.minecraft = Some("1.20.1".to_string());
|
||||
manifest.overrides.push(PackOverrides {
|
||||
dir: "overrides".to_string(),
|
||||
side: PackSide::Both,
|
||||
files: vec![OverrideFile {
|
||||
path: "mods/missing.jar".to_string(),
|
||||
sha1: "0".repeat(40),
|
||||
size: 1,
|
||||
}],
|
||||
});
|
||||
|
||||
let root = temp_root("export-half");
|
||||
std::fs::create_dir_all(&root).expect("建目录");
|
||||
let target = root.join("out.zip");
|
||||
let error = CURSEFORGE
|
||||
.write(&manifest, PackPayload::from_root(&root), &target)
|
||||
.expect_err("覆盖文件不存在时必须失败");
|
||||
assert_eq!(error.kind(), PackErrorKind::Io, "{error}");
|
||||
assert!(!target.exists(), "失败后目标路径不许存在");
|
||||
|
||||
let leftovers: Vec<String> = std::fs::read_dir(&root)
|
||||
.expect("列目录")
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
assert!(
|
||||
leftovers.is_empty(),
|
||||
"临时文件没清干净(p3-1 回退):{leftovers:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_export_is_still_allowed() {
|
||||
// 反向对照:这两条闸不能把同格式往返也挡掉(那是 r1 明确要保住的)。
|
||||
let fixture = fixtures::curseforge("export-same-cf");
|
||||
let pack = PackArchive::open_default(&fixture.zip).expect("打开 CF 夹具");
|
||||
let manifest = CURSEFORGE.read(&pack).expect("读 CF 清单");
|
||||
CURSEFORGE
|
||||
.check_overrides(&manifest)
|
||||
.expect("CF 自己读出来的模型必须能被 CF 表达");
|
||||
|
||||
let fixture = fixtures::modrinth("export-same-mr");
|
||||
let pack = PackArchive::open_default(&fixture.zip).expect("打开 mrpack 夹具");
|
||||
let manifest = MODRINTH.read(&pack).expect("读 mrpack 清单");
|
||||
MODRINTH
|
||||
.check_overrides(&manifest)
|
||||
.expect("mrpack 自己读出来的模型必须能被 mrpack 表达");
|
||||
}
|
||||
@@ -35,9 +35,11 @@ impl PackFormat for ToyFormat {
|
||||
Some("toy.json")
|
||||
}
|
||||
|
||||
fn detect(&self, pack: &PackArchive) -> Option<&'static str> {
|
||||
let value = pack.read_json("toy.json").ok().flatten()?;
|
||||
value.is_object().then_some("toy.json")
|
||||
fn detect(&self, pack: &PackArchive) -> PackResult<Option<&'static str>> {
|
||||
let Some(value) = pack.read_json("toy.json")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(value.is_object().then_some("toy.json"))
|
||||
}
|
||||
|
||||
fn read(&self, pack: &PackArchive) -> PackResult<PackManifest> {
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
//! **全部离线**:没有网络调用,也没有真实整合包样本(见 `fixtures` 的来源声明)。
|
||||
|
||||
mod detect;
|
||||
mod export;
|
||||
mod extend;
|
||||
mod fixtures;
|
||||
mod model;
|
||||
mod roundtrip;
|
||||
mod safety;
|
||||
mod unknown_fields;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! 模型级辅助与读取预算:r1 p2-2(跨侧重名)与 p3-4(批量读取的累计限额)。
|
||||
|
||||
use super::fixtures;
|
||||
use crate::addon::pack::archive::{PackArchive, ReadBudget};
|
||||
use crate::addon::pack::error::PackErrorKind;
|
||||
use crate::addon::pack::manifest::{OverrideFile, PackManifest, PackOverrides, PackSide};
|
||||
|
||||
fn file(path: &str, sha1: &str, size: u64) -> OverrideFile {
|
||||
OverrideFile {
|
||||
path: path.to_string(),
|
||||
sha1: sha1.to_string(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_override_paths_counts_across_sides_not_per_side() {
|
||||
// r1 p2-2:mrpack 的真实冲突是 `overrides`(Both) 与 `client-overrides`(Client) 撞同一路径,
|
||||
// 按 `(side, path)` 计数时两侧键天然不同 ⇒ 永远返回空表。
|
||||
let mut both = PackOverrides::new("overrides", PackSide::Both);
|
||||
both.files.push(file("config/x.toml", "a", 1));
|
||||
both.files.push(file("config/only-both.toml", "b", 1));
|
||||
let mut client = PackOverrides::new("client-overrides", PackSide::Client);
|
||||
client.files.push(file("config/x.toml", "c", 2));
|
||||
let mut server = PackOverrides::new("server-overrides", PackSide::Server);
|
||||
server.files.push(file("config/only-server.toml", "d", 3));
|
||||
let manifest = PackManifest {
|
||||
overrides: vec![both, client, server],
|
||||
..PackManifest::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
manifest.duplicate_override_paths(),
|
||||
vec![(
|
||||
"config/x.toml".to_string(),
|
||||
vec![PackSide::Both, PackSide::Client]
|
||||
)],
|
||||
"只报真正撞车的路径,并给出涉及的侧"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_override_paths_is_empty_when_nothing_collides() {
|
||||
let mut both = PackOverrides::new("overrides", PackSide::Both);
|
||||
both.files.push(file("config/a.toml", "a", 1));
|
||||
let mut client = PackOverrides::new("client-overrides", PackSide::Client);
|
||||
client.files.push(file("config/b.toml", "b", 1));
|
||||
let manifest = PackManifest {
|
||||
overrides: vec![both, client],
|
||||
..PackManifest::default()
|
||||
};
|
||||
assert!(manifest.duplicate_override_paths().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_batch_scan_spends_one_cumulative_budget() {
|
||||
// r1 p3-4:打开阶段只能看声明大小;批量读取(override_files)必须按**实际字节**累计扣减,
|
||||
// 否则"很多条各自合规"累起来就能绕过 `max_uncompressed_bytes`(CPU/时间 DoS)。
|
||||
let fixture = fixtures::modrinth("budget");
|
||||
let pack = PackArchive::open_default(&fixture.zip).expect("打开 mrpack 夹具");
|
||||
|
||||
let generous = ReadBudget::new(1024 * 1024);
|
||||
let files = pack
|
||||
.override_files_with("overrides", &generous)
|
||||
.expect("预算够时必须成功");
|
||||
assert_eq!(files.len(), 2);
|
||||
|
||||
// 预算只有 4 字节:第一个文件(config/known.toml,15 字节)就该把预算吃穿。
|
||||
let tight = ReadBudget::new(4);
|
||||
let error = pack
|
||||
.override_files_with("overrides", &tight)
|
||||
.expect_err("累计预算必须生效");
|
||||
assert_eq!(error.kind(), PackErrorKind::Limits, "{error}");
|
||||
assert_eq!(tight.remaining(), 0, "失败后预算不该被退回");
|
||||
}
|
||||
@@ -247,7 +247,7 @@ fn curseforge_without_name_is_a_metadata_error() {
|
||||
);
|
||||
let pack = PackArchive::open_default(&target).expect("zip 本身可读");
|
||||
assert_eq!(
|
||||
CURSEFORGE.detect(&pack),
|
||||
CURSEFORGE.detect(&pack).expect("清单可读"),
|
||||
Some("manifest.json"),
|
||||
"形状对得上就该被探测到"
|
||||
);
|
||||
|
||||
@@ -272,3 +272,41 @@ fn directory_source_rejects_symlinks_when_the_platform_can_make_them() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn the_second_check_catches_windows_device_names_and_dot_suffixes() {
|
||||
// r1 p2-3:第一检(平台无关的七条)对这些名字是**放行**的,拦下它们的是第二检
|
||||
// `enclosed_name()` 的 Win32 命名空间规则(DOS 设备名、结尾点/空格、`< > " | ? *`)。
|
||||
// 删掉第二检,这条用例必须红——这是「双检各有一条恶意样本」的第二条。
|
||||
for (tag, name) in [
|
||||
("dev-con", "CON"),
|
||||
("dev-nul-nested", "mods/NUL"),
|
||||
("dev-com1", "COM1.txt"),
|
||||
("dev-trailing-dot", "a."),
|
||||
("dev-trailing-space", "mods/b "),
|
||||
("dev-wildcard", "mods/*.jar"),
|
||||
] {
|
||||
let target = single_entry(tag, name);
|
||||
let error = open_error(&target);
|
||||
assert_eq!(
|
||||
error.kind(),
|
||||
PackErrorKind::Traversal,
|
||||
"{name}: {}",
|
||||
error.message()
|
||||
);
|
||||
assert!(
|
||||
error.message().contains("enclosed_name"),
|
||||
"{name} 必须是被第二检拦下的(不然这条用例钉错了地方):{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
|
||||
// 对照:第一检对这些名字没有任何意见(所以它们只能靠第二检)。
|
||||
for name in ["CON", "mods/NUL", "a.", "mods/b ", "mods/*.jar"] {
|
||||
assert!(
|
||||
crate::addon::pack::path::normalize_file(name).is_ok(),
|
||||
"第一检不该拦 {name}(拦了这条用例就失去意义)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! 「未知字段不丢」的三处条目级 flatten(r1 p3-3):`env_extra`、CF `files[].extra`、
|
||||
//! `PackLoader::extra`。三处实现本来是对的,但此前一条断言都没有——而它们恰好是 M115 点名的
|
||||
//! HMCL 回写 `fileName`/`url` 的位置。每条都走「读 → render → 写整包 → 再读逐字段相等」。
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::addon::pack::archive::PackArchive;
|
||||
use crate::addon::pack::curseforge::CURSEFORGE;
|
||||
use crate::addon::pack::format::{PackFormat, PackPayload};
|
||||
use crate::addon::pack::modrinth::MODRINTH;
|
||||
|
||||
#[test]
|
||||
fn mrpack_env_unknown_keys_survive_read_write_reread() {
|
||||
// r1 p3-3 的一处:`files[].env` 里的未知键(`env_extra`)此前没有任何断言。
|
||||
let root = super::temp_root("env-extra");
|
||||
let dir = root.join("src");
|
||||
super::write_file(
|
||||
&dir.join("modrinth.index.json"),
|
||||
br#"{"game":"minecraft","formatVersion":1,"versionId":"1.0","name":"env extra keys",
|
||||
"files":[{"path":"mods/a.jar",
|
||||
"hashes":{"sha512":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
|
||||
"env":{"client":"required","server":"unsupported","customEnvKey":{"k":1}},
|
||||
"downloads":["https://example.invalid/a.jar"]}],
|
||||
"dependencies":{"minecraft":"1.20.1"}}"#,
|
||||
);
|
||||
let zip = root.join("pack.mrpack");
|
||||
super::zip_dir(&dir, &zip);
|
||||
|
||||
let pack = PackArchive::open_default(&zip).expect("打开包");
|
||||
let manifest = MODRINTH.read(&pack).expect("读 mrpack 清单");
|
||||
assert_eq!(manifest.files[0].env_extra["customEnvKey"], json!({"k": 1}));
|
||||
|
||||
let target = root.join("out.mrpack");
|
||||
MODRINTH
|
||||
.write(&manifest, PackPayload::metadata_only(), &target)
|
||||
.expect("导出 mrpack");
|
||||
let reread = MODRINTH
|
||||
.read(&PackArchive::open_default(&target).expect("打开导出包"))
|
||||
.expect("再读");
|
||||
assert_eq!(reread, manifest, "env 里的未知键必须原样回写");
|
||||
assert_eq!(reread.files[0].env_extra["customEnvKey"], json!({"k": 1}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curseforge_file_unknown_keys_survive_read_write_reread() {
|
||||
// r1 p3-3 的另一处:CF `files[].fileName` / `url` 是 HMCL 补全后回写的扩展点。
|
||||
let root = super::temp_root("cf-file-extra");
|
||||
let dir = root.join("src");
|
||||
super::write_file(
|
||||
&dir.join("manifest.json"),
|
||||
br#"{"manifestType":"minecraftModpack","manifestVersion":1,"name":"files extra keys",
|
||||
"version":"1.0","minecraft":{"version":"1.20.1","modLoaders":[{"id":"forge-47.2.0","primary":true}]},
|
||||
"files":[{"projectID":238222,"fileID":5102025,"required":true,
|
||||
"fileName":"jei-1.20.1-15.2.0.27.jar",
|
||||
"url":"https://edge.forgecdn.net/files/5102/25/jei.jar"}]}"#,
|
||||
);
|
||||
let zip = root.join("pack.zip");
|
||||
super::zip_dir(&dir, &zip);
|
||||
|
||||
let pack = PackArchive::open_default(&zip).expect("打开包");
|
||||
let manifest = CURSEFORGE.read(&pack).expect("读 CF 清单");
|
||||
assert_eq!(
|
||||
manifest.files[0].extra["fileName"],
|
||||
json!("jei-1.20.1-15.2.0.27.jar")
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.files[0].extra["url"],
|
||||
json!("https://edge.forgecdn.net/files/5102/25/jei.jar")
|
||||
);
|
||||
|
||||
let target = root.join("out.zip");
|
||||
CURSEFORGE
|
||||
.write(&manifest, PackPayload::metadata_only(), &target)
|
||||
.expect("导出 CF");
|
||||
let reread = CURSEFORGE
|
||||
.read(&PackArchive::open_default(&target).expect("打开导出包"))
|
||||
.expect("再读");
|
||||
assert_eq!(reread, manifest, "files[] 里的未知键必须原样回写");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curseforge_loader_unknown_keys_survive_read_write_reread() {
|
||||
// r1 p3-3 的第三处:`modLoaders[]` 里的未知键(`PackLoader::extra`)。
|
||||
let root = super::temp_root("cf-loader-extra");
|
||||
let dir = root.join("src");
|
||||
super::write_file(
|
||||
&dir.join("manifest.json"),
|
||||
br#"{"manifestType":"minecraftModpack","manifestVersion":1,"name":"loader extra keys",
|
||||
"version":"1.0",
|
||||
"minecraft":{"version":"1.20.1",
|
||||
"modLoaders":[{"id":"forge-47.2.0","primary":true,"cachedName":"Forge 47.2.0"}]},
|
||||
"files":[]}"#,
|
||||
);
|
||||
let zip = root.join("pack.zip");
|
||||
super::zip_dir(&dir, &zip);
|
||||
|
||||
let pack = PackArchive::open_default(&zip).expect("打开包");
|
||||
let manifest = CURSEFORGE.read(&pack).expect("读 CF 清单");
|
||||
assert_eq!(
|
||||
manifest.loaders[0].extra["cachedName"],
|
||||
json!("Forge 47.2.0")
|
||||
);
|
||||
|
||||
let target = root.join("out.zip");
|
||||
CURSEFORGE
|
||||
.write(&manifest, PackPayload::metadata_only(), &target)
|
||||
.expect("导出 CF");
|
||||
let reread = CURSEFORGE
|
||||
.read(&PackArchive::open_default(&target).expect("打开导出包"))
|
||||
.expect("再读");
|
||||
assert_eq!(reread, manifest, "modLoaders[] 里的未知键必须原样回写");
|
||||
assert_eq!(reread.loaders[0].extra["cachedName"], json!("Forge 47.2.0"));
|
||||
}
|
||||
Reference in new issue
Block a user