feat(core): 整合包格式无关模型与格式扩展点(M110)

从零建 `core::addon::pack`:格式无关中间模型 + 格式扩展点 + 安全读取层,
不碰网络、不碰 UI(core-no-ui 依赖表未变)。

- `PackManifest` 无损表达 CF / mrpack / MultiMC 三方字段:包名/版本/作者/简介/
  格式版本/游戏版本/加载器依赖/文件条目/覆盖目录;`files[].hashes` 保留整张摘要表
  (mrpack 允许 sha1+sha256+sha512+md5 共存),`disabled` 记语义(导出按 D10 映射成
  env.client=optional,CF 落 required=false)。
- 未知字段按条目用 serde flatten 接住(清单顶层 / CF `minecraft` 对象 / 文件条目 /
  env / 加载器条目),导出时并回且已知字段优先;两格式各有一条「读→写→再读相等」测试。
- `trait PackFormat`(id / label / metadata_entry / detect -> Option<证据> /
  read / render / 默认 write)+ `PackRegistry`:注册表顺序即优先级,写死
  CurseForge → Modrinth → MultiMC;同一份包命中多格式时 `also_matched` 暴露歧义。
  测试里用一个 toy 格式(单文件、一行注册)证明扩展点真的可扩展。
- `PackArchive` 把目录与 zip 统一成规整条目表:路径穿越双检(自研七条规则 +
  zip crate 的 enclosed_name)、符号链接条目拒绝、条目数/解压体积/单条目上限、
  规整后重名拒绝、篡改载荷不当场吞掉;每条都有恶意样本测试。
- 格式实现:CF 可读写(缺路径如实记 None)、mrpack 可读写(强制 sha512 形状、
  env 三态、三个覆盖目录成三侧)、MultiMC 只读(.minecraft 当覆盖目录,
  组件顺序与非加载器组件全保留)。
- 三种格式各一个手造夹具(含截断 TOML / 半截 jar / 零字节 jar),来源在
  `tests/fixtures.rs` 里声明为手造,不冒充真实导出。
This commit is contained in:
AskaEth committed 2026-11-04 00:20:24 +08:00
1 parent 40a4c06d42
commit b34e86bba5
20 files changed
+3729 -1

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
//! 整合包的**格式无关模型**与**格式扩展点**(M110)。导入/导出管线(M111/M112)建在这上面。
//!
//! 设计定稿:`DevDocs/PlanDoc/08-mod-and-recommend.md` §2(D10),格式细节来自
//! `DevDocs/analyse/05-modloader-modpack-p2.md` §2.1–§2.3(HMCL 源码级拆解,A 级来源)。
//!
//! # 本模块只做三件事
//!
//! | 层 | 模块 | 职责 |
//! |----|------|------|
//! | 安全读取 | [`archive`] | 目录与 zip 统一成"规整过的条目表":路径穿越双检、符号链接拒绝、条数/体积上限 |
//! | 格式 | [`format`] | [`format::PackFormat`] trait + [`format::PackRegistry`]:探测、读、渲染、写,**优先级写死** |
//! | 模型 | [`manifest`] | [`manifest::PackManifest`]:包名/版本/作者/游戏版本/加载器/文件条目/覆盖目录,未知字段原样保留 |
//!
//! 三个内置格式:[`curseforge`](`manifest.json`,可读可写)、[`modrinth`](`.mrpack`,可读可写)、
//! [`multimc`](`mmc-pack.json`,**只读**)。
//!
//! # 刻意不做的事(分工边界)
//!
//! - **不发网络请求**:CF 的 `projectID/fileID` → 文件名/URL 补全属于 M111(要用 CF API 与
//! MurmurHash2 指纹),本模块只把"缺什么"如实记成 `None`;
//! - **不落盘解压**:`archive` 只读,不写实例目录。overrides 的三方合并(base/theirs/ours)
//! 是 M111 的解压器职责,本模块提供它要的输入:每条覆盖文件的实例内相对路径 + SHA-1;
//! - **不碰 UI 类型**:core 零 UI 依赖(`core-no-ui` 门禁),这里连 `String` 之外的表现层
//! 类型都不出现;
//! - **不猜格式**:探测顺序写死在 [`format::PackRegistry::builtin`],同一份包命中多格式时
//! 用固定的注册顺序选,并把其余命中记进 [`format::PackMatch::also_matched`]。
//!
//! # 安全(每条都有恶意样本测试,见 `tests/safety.rs`)
//!
//! 路径穿越判据是**两条独立检查**:①本模块的 [`path::normalize_file`](七条写死的规则:
//! 空名/`\0`/`\`/`:`/前导 `/`/任何 `..` 段/规整后为空);②`zip` crate 的
//! `enclosed_name()`(按 Windows 规则解析盘符、NTFS 数据流、保留设备名)。
//! 为什么不能只留一条:`enclosed_name()` 允许"深度 0 的根目录",也就是 `"/etc/passwd"`
//! 它会放行;反过来,本模块的规则不认 Windows 保留设备名。**两条都过才算安全**。
//!
//! # 测试夹具的来源(不许把"看起来像"当真实)
//!
//! `tests/fixtures.rs` 里的 CF / mrpack / MultiMC 三个包**全部是手造的**:字段取自
//! analyse/05 记录的 HMCL 解析器形状(A 级来源),内容是为了覆盖边界而编的(含一个刻意的
//! 损坏文件:截断的 `config/broken.toml`、半截 `mods/half.jar`、零字节 `mods/broken.jar`)。
//! **没有一个来自真实导出**——本任务环境离线,没有真实整合包样本可对照。真实样本的字段
//! 偏移风险因此只能由 M111/M115(格式字段盘点)在拿到真包后回收,报告里已登记这条欠账。
pub mod archive;
pub mod curseforge;
pub mod error;
pub mod format;
pub mod manifest;
pub mod modrinth;
pub mod multimc;
pub mod path;
#[cfg(test)]
mod tests;
pub use archive::{PackArchive, PackEntryInfo, PackLimits};
pub use curseforge::CurseForge;
pub use error::{PackError, PackErrorKind, PackResult};
pub use format::{PackFormat, PackMatch, PackPayload, PackRegistry};
pub use manifest::{
EnvSupport, OverrideFile, PackFile, PackLoader, PackManifest, PackOverrides, PackSide,
};
pub use modrinth::Modrinth;
pub use multimc::MultiMc;