Files
Unlucky-Launcher/crates/core/src/addon/export.rs
T
AskaEth 90b233088f feat(core): M112 整合包导出(mrpack 优先 + CF 清单,逐文件远程判定与内联兜底)
新增 addon::export(实例目录 → .mrpack / CF manifest.json):
- 逐文件三分:远程引用 / 内联进 overrides/ / 跳过(读不了、符号链接、路径不安全)。
  「能否被远程替代」= 目录能证明身份(Modrinth SHA-1、CF 指纹)**且**目标格式表达得了;
  证明不了的一律内联,绝不丢、绝不编,逐条进结构化报告(ExportReport)。
- game / dependencies 来自实例快照:versions/<id>/<id>.json 的继承链「叶子」+ 库坐标推加载器,
  不解析 <mc>-<loader>-<ver> 字符串(社区实例名不统一,那是猜)。
- .disabled 走路径后缀(不是 env.client=optional:后者装回来默认启用,跨启动器不保真)。
- CF files[] 只写能确定 projectID/fileID 的项;禁用文件 CF 表达不了,内联并记原因。
  CF modLoaders[].id 按官方写法:NeoForge 在 1.20.1 上写 neoforge-1.20.1-<ver>。
- 尺寸护栏:fileSize 超 u32 报 pack_limits;整包撞 ZIP64 阈值由 format::write 拒绝。

配套改动:
- pack/curseforge/fingerprint:MurmurHash2-32 + 0x09/0x0A/0x0D/0x20 过滤、seed=1、
  长度用过滤后长度、哨兵 811513880 短路;金标准由一份独立 Java 实现算出(双向验证)。
- pack/format/payload:PackPayload 加 with_sources(包内条目名 → 实例源文件),
  让「实例里的 mods/x.jar → 包里的 overrides/mods/x.jar」不用拼路径。
- pack/format:写盘时按未压缩总量守 ZIP64 阈值。
- pack/modrinth:禁用位改写路径后缀;fileSize 按 u32 校验。
- pack/manifest:PackLoader::curse_id_for 处理 NeoForge 的 MC 前缀特例。

测试:导出管线 10 条 + 往返保真 4 条(三种格式夹具;允许的差异逐条在测试文件头登记,
并在报告 notes 里给出)。M111 未合并,故本轮「导入」用 M110 的 read + 同形实例目录,
真 import_pack → export → import_pack 端到端留作欠账。
2026-11-04 21:32:37 +08:00

66 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 整合包**导出**(M112):实例目录 → `.mrpack`(优先)/ CurseForge `manifest.json`(次之)。
//!
//! 设计依据:`DevDocs/PlanDoc/08-mod-and-recommend.md` §2(D10)、
//! `DevDocs/SubAgentsTalk/addon-整合包导入导出对接要点.md` §4(导出八条),格式模型与安全闸
//! 是 M110 的 [`crate::addon::pack`],本模块**只消费,不重写**。
//!
//! # 一句话口径:不认识的 mod 不能丢、不能编
//!
//! 用户原话(M112 任务书):「要么能证明它可被远程替代,要么就必须内联进 `overrides/` 并在
//! 结果里逐条报告」。落到代码就是三分类,**没有第四类**:
//!
//! | 分类 | 判据 | 产物 |
//! |------|------|------|
//! | 远程引用([`FileClass::Remote`]) | 目标格式**能表达**它 + 某个目录([`RemoteCatalog`])**能证明**它的身份 | 清单 `files[]` 一条 |
//! | 内联([`FileClass::Inline`]) | 证明不了,或证明了但目标格式表达不了 | 进 `overrides/`,原字节照搬 |
//! | 跳过([`FileClass::Skipped`]) | 读不了 / 是符号链接 / 路径不安全 | 不写进包,**逐条**进报告 |
//!
//! 「能证明」用两条独立查询(契约 §4.4 的顺序):**Modrinth SHA-1** →
//! [`RemoteCatalog::modrinth_by_sha1`];**CurseForge 指纹**([`crate::addon::pack::curseforge::fingerprint`])
//! → [`RemoteCatalog::curseforge_by_fingerprint`]。两条都命中时两条直链都进 `downloads[]`
//! (HMCL 同款,兼容面更大)。**没有网络时**用 [`OfflineCatalog`]:所有文件都内联,包照样完整可导入
//! ——这不是降级失败,是"证明不了就内联"的正面行为。
//!
//! # 每条格式决策落在哪(tower 定案,别按任务书原话改回去)
//!
//! 1. **`.disabled` 走路径后缀**(`mods/x.jar.disabled`),不是 `env.client=optional`:后者只是
//! "安装时可取消勾选",装回来默认仍然启用 ⇒ 跨启动器不保真。官方 Modrinth 导出也是后缀。
//! 2. **覆盖目录一律 `overrides/`**(不用 HMCL 那套 `client-overrides/`):Modrinth 官方与
//! Prism 都用它,兼容面最大。分侧语义在本轮导出里不产生(实例目录本身不分侧)。
//! 3. **`game` + `dependencies` 从实例快照取**,绝不只写 `minecraft` 漏加载器:快照来自
//! `versions/<id>/<id>.json` 的继承链 + 库坐标([`snapshot`]),**不解析**
//! `<mc>-<loader>-<ver>` 这类实例 id 字符串(社区实例名不统一,那是猜)。
//! 4. **CF `files[]` 只写能确定 `projectID`/`fileID` 的项**:CF 没有"内联到 files[]"的概念,
//! 证明不了的(以及**禁用文件**:CF 根本没有禁用表达)一律进 `overrides/` 并进报告。
//! 5. **尺寸护栏**:`fileSize` 是 u32(超了报 `pack_limits`,不静默截断)、整包撞 ZIP64 阈值由
//! `format::write` 拒绝(`pack_limits`,不留半个包)。
//!
//! # 刻意不做的事(分工边界,别以为是漏做)
//!
//! - **MultiMC / Prism 导出**:不做(P1 只做导入)。它的包是"整实例快照",要先把所有远程文件
//! 下载下来再打包,成本极高;导入侧 M111 已覆盖。**这是有意省略。**
//! - **不装加载器 / 不补全清单**:加载器元数据解析与库下载是 `addon::loader` + `game::install`;
//! CF 的 `projectID/fileID → 文件名/URL` 补全需要 CF 客户端(D10 §5 规划中),本轮由调用方注入
//! [`RemoteCatalog`],本模块不发网络请求。
//! - **不碰 UI 类型**:core 零 UI 依赖(`core-no-ui` 门禁),报告只出现 `String` / `&'static str`。
//! - **不改实例**:导出只读实例目录。
//!
//! # 没验的部分(报告里同样登记)
//!
//! 真实 Modrinth / CurseForge API 查询(要网络与 key,`RemoteCatalog` 目前只有离线实现)、
//! 超大实例(GB 级、上千文件——指纹要对每个文件多读两遍)、Windows 之外的平台、
//! 以及「M111 合并后的 `import_pack → export → import_pack` 真端到端」。
pub mod catalog;
pub(crate) mod classify;
pub mod pipeline;
pub mod report;
pub mod scan;
pub mod snapshot;
pub use catalog::{OfflineCatalog, RemoteCatalog, RemoteHit, RemoteSource};
pub use pipeline::{export_instance, ExportRequest};
pub use report::{ExportFile, ExportReport, FileClass, FileReason};
pub use scan::{scan_instance, LocalFile, SkippedLocal, DEFAULT_CONTENT_ROOTS, OVERRIDES_DIR};
pub use snapshot::{read_instance_snapshot, InstanceSnapshot, SnapshotLoader};