通用工具兼容性
结论摘要
前置的 32 字节 Header 不影响通用工具打开;真正的风险在文件末尾 —— Footer 与恢复记录位于 EOCD 之后,二者合计超过 65535 字节时, bsdtar 与 Python zipfile 会因只回扫文件末尾 64 KB 而找不到 EOCD。 7-Zip 因搜索范围更宽而不受影响。
为什么要验证这件事
规范的设计前提里有一条:
本格式基于标准 ZIP64 构建,通用压缩工具可以正常解压并读出全部条目名列表。
这是格式的核心承诺。若不成立,那么"可排障性""生态互操作性""零成本迁移"三条设计理由 全部落空,格式应重新设计。因此这里用可复现的实验验证该前提,并界定它的成立条件。
测试环境
| 项目 | 版本 / 说明 |
|---|---|
| 操作系统 | Windows 10/11 (x64) |
| 7-Zip | 26.03 (x64) |
| bsdtar | Windows 内置 tar.exe |
| Python | 3.13.12(zipfile 标准库) |
| 测试文件 | 按规范手工构造的等价容器 |
测试结果
基准是一个含 2 个条目的标准 ZIP,在此之上叠加变量构造变体。
| 变体 | 7-Zip l | 7-Zip x | bsdtar | Python zipfile |
|---|---|---|---|---|
| 纯 ZIP(基准) | 通过 | 通过 | 通过 | 通过 |
| Header + 偏移已修正 | 通过 | 通过 | 通过 | 通过 |
| Header + 偏移未修正 | 通过 | 通过 | 通过 | 通过 |
| Header + ZIP + Footer 24B | 通过 | 通过 | 通过 | 通过 |
| Footer 计入 EOCD 注释长度 | 通过 | 通过 | 通过 | 通过 |
| Header + ZIP + 64 KB 恢复记录 + Footer | 通过 | 通过 | 失败 | 失败 |
| Header + ZIP + 1 MB 恢复记录 + Footer | 通过 | 通过 | 失败 | 失败 |
| 纯 ZIP + Footer(隔离 Header 变量) | 通过 | 通过 | 通过 | 通过 |
| 强制 ZIP64 + Header(偏移已修正) | 通过 | 通过 | 通过 | 通过 |
| 强制 ZIP64 + Header(偏移未修正) | 通过 | 通过 | 通过 | 通过 |
| 强制 ZIP64(无 Header) | 通过 | 通过 | 通过 | 通过 |
分界线非常干脆:只有"EOCD 之后的数据超过 64 KB"这一种情况会失败。
失败时的报错:
[tar] tar: Error opening archive: Unrecognized archive format
[python] BadZipFile: File is not a zip file而同一个文件 7-Zip 仍能正常列出全部条目。
原理分析
前置数据为什么不影响
在文件开头插入自定义数据,在 ZIP 领域有成熟先例 —— 自解压归档(SFX)。机制是:
- ZIP 的权威索引是文件末尾的 EOCD,工具从末尾向前搜索签名
PK\x05\x06,而不是从开头解析 - EOCD 中记录的中央目录偏移是相对文件起始的绝对偏移
- 因此只要把偏移字段整体后移前缀长度,工具就能正确定位中央目录
7-Zip 的输出直接印证了它按 SFX 语义处理该文件:
Type = zip
Embedded Stub Size = 32 <- 明确识别出 32 字节前缀偏移修正:必要性来自规范,不是可用性
这是实测中最反直觉的发现:三个工具在偏移未修正时全都能打开。
原因是它们都实现了同一套前缀补偿逻辑:
前缀长度 = EOCD 在文件中的实际位置 - 中央目录大小 - EOCD 中记录的中央目录偏移7-Zip 甚至会把推算结果直接打印出来:
Type = zip
Offset = 32 <- 自行推算出的前缀长度但这不构成可以不修正的理由:
- 这是各实现的宽容行为,不是 ZIP 规范的要求
- 依赖宽容行为会掩盖真正的偏移错误 —— 例如条目级偏移写错时,工具可能仍"看起来正常", 问题被推迟到特定条目解压时才暴露
规范中的"必须修正"依然正确;实现指南建议的打包后自检依然必要。
真正的风险:EOCD 之后的尾随数据
ZIP 规范规定 EOCD 是文件的最后一个结构,其后只允许注释, 且注释长度记录在 EOCD 的 commentLength 字段(2 字节,最大 65535)。 因此主流实现的定位策略是只回扫文件末尾 64 KB。
而 .mfp 的 Footer 位于 EOCD 之后、恢复记录位于 Footer 之前,于是产生隐含上限:
recoveryLength + 24(Footer) <= 65535- 不超过:严格实现仍能在 64 KB 窗口内找到 EOCD,正常打开
- 超过:EOCD 被推出搜索窗口,bsdtar 与 Python
zipfile报"不是压缩包"
7-Zip 因搜索范围更宽,不受此限制 —— 这也解释了为什么它能通过全部变体。
缺陷定级
| 项目 | 评估 |
|---|---|
| 缺陷 | Footer / 恢复记录位于 EOCD 之后,超出 ZIP 规范的注释长度上限 |
| 触发条件 | recoveryLength + 24 > 65535,即恢复记录超过约 64 KB |
| 影响面 | bsdtar、Python zipfile 等按严格逻辑实现的工具与库;7-Zip 不受影响 |
| 严重性 | 中。默认未启用恢复记录时不触发;但恢复记录是可选特性,一旦启用大尺寸即失效 |
| 文档问题 | 原表述未标注此约束,属表述过于绝对 |
同时应指出:若把恢复记录计入 EOCD.commentLength,在 64 KB 以内是完全合规的 ZIP 注释,任何工具都不会拒绝。这提供了一个低成本的兼容区间。
修复方案
方案 A:仅修正文档表述(成本最低)
把"可以直接打开并解压"改为带条件的表述,明确标注恢复记录带来的尺寸约束。
- 优点:零改动成本
- 缺点:把约束转嫁给使用者,格式的"可排障性"承诺在启用恢复记录后打折
方案 B:限制恢复记录尺寸(低成本折中)
规定 recoveryLength <= 65511(65535 − 24),并把恢复记录与 Footer 计入 EOCD.commentLength。
- 优点:完全符合 ZIP 规范,所有工具都能打开,实现改动小
- 缺点:恢复记录容量被限制在 64 KB,对需要覆盖中央目录与 manifest 的大容器可能不够
方案 C:把 Footer 与恢复记录移入 ZIP 内部(推荐)
不再把 Footer 放在 EOCD 之后,而是作为普通 ZIP 条目存放:
[Local File Header] .mfp-footer <- 承载 Footer Descriptor 与恢复记录
[Local File Header] manifest.json
[Local File Header] fonts/<sha256>...
[Central Directory][ZIP64 EOCD][Locator][EOCD] <- 文件末尾,无任何尾随数据读取端改为:从中央目录取出 .mfp-footer 条目并解析,而不是检查 EOCD 紧接着的 24 字节。
- 优点:EOCD 恒为文件最后一个结构,彻底消除 64 KB 限制;恢复记录不再有尺寸上限; 结构更统一(所有扩展数据都走 ZIP 条目,与
manifest.json一致) - 缺点:需修改规范的 Footer、恢复记录与格式识别流程,客户端读取逻辑需同步调整
推荐组合:以方案 C 为目标,配合方案 A 先行修正文档表述作为过渡。
未验证项与局限
| 项 | 说明 |
|---|---|
无真实 .mfp 样本 | 全盘检索未发现任何 .mfp 文件;结论基于按规范手工构造的等价容器 |
| 客户端尚未实现 | 实现指南的验收清单该条仍为未勾选状态 |
| WinRAR 无命令行数据 | Rar.exe 无法用于测试 ZIP;WinRAR GUI 行为未取得实测证据,结论为推理 |
| 未覆盖加密容器 | 规范提及 WinZip AES;加密后通用工具会提示输入密码,行为与"打不开"易混淆 |
| 未覆盖数据描述符场景 | 流式写入(bit 3 标志 + data descriptor)的容器未构造测试,预计无影响 |
| 7-Zip 版本差异 | 实测使用 26.03,较旧版本行为可能不同 |
在缺少失败样本的情况下,未能复现"7-Zip 打不开"的现象。若后续取得该文件,应优先做三件事:
- 用
7z l -slt检查其Type与Embedded Stub Size - 用十六进制查看器确认 EOCD 距文件末尾的实际字节数
- 检查 EOCD 的
commentLength字段与实际尾随数据长度是否一致
结论
- 规范的核心设计前提成立:按规范修正偏移后,前置 32 字节 Header 的容器可被 7-Zip 正常打开与解压。
- 偏移修正的必要性是"规范性"而非"可用性":三者对未修正偏移的文件也能打开, 规范要求修正依然正确,但不应把工具的宽容当作格式正确性的依据。
- 存在一个真实缺陷:Footer 与恢复记录位于 EOCD 之后,超过 65535 字节时 bsdtar 与 Python
zipfile无法定位 EOCD。这是"通用工具可打开"承诺的唯一失效点。 - 文档表述需要修正,或采纳方案 C 从结构上消除该约束。