Skip to content

容器结构 ​

整体布局 ​

偏移 0
┌────────────────────────────────────────────────┐
│  MFP Header(固定 32 字节,magic = "MFP\x01")   │
├────────────────────────────────────────────────┤
│  Local File Header + manifest.json(deflate)   │  <- manifestOffset 指向这里
├────────────────────────────────────────────────┤
│  Local File Header + fonts/<sha256>.ttf(store) │
├────────────────────────────────────────────────┤
│  Central Directory -> ZIP64 EOCD -> Locator -> EOCD │
├────────────────────────────────────────────────┤
│  恢复记录(可选,变长)                            │
├────────────────────────────────────────────────┤
│  MFP Footer Descriptor(固定 24 字节)           │
└────────────────────────────────────────────────┘ 文件末尾

一句话读法:头部认格式,中间是普通 ZIP,尾部放校验与恢复信息。

MFP Header(32 字节) ​

固定长度,位于偏移 0,是识别 .mfp 的唯一依据。

字段大小说明
magic4 字节4D 46 50 01,即 MFP\x01
formatVersion2 字节主版本号,兼容性判定依据
flags2 字节标志位
manifestOffset8 字节manifest.json 条目在文件中的偏移
其余16 字节保留

兼容性看的是 formatVersion

判断能否读取应依据 formatVersion(主版本号),不是 mfpVersion。 后者是产包工具或客户端的版本,与格式兼容性无关。

manifest.json ​

包内唯一的索引文件,描述全部字体、模板、校验与冲突信息。它本身是一个普通 ZIP 条目 (deflate 压缩),因此可以被任何 ZIP 工具直接取出查看。

用 draft 2020-12 的 JSON Schema 定义,可直接被各语言实现引用。最小可用形态只描述单个字体, 完整形态还包含集合字体、预览模板、冲突记录与恢复记录索引。

条目命名 ​

字体条目以内容的 SHA-256 命名(例如 fonts/9f86d081....ttf)。这样做带来两个直接好处:

  1. 天然去重 —— 同一份字体重复加入只会产生一个条目
  2. 可校验 —— 条目名本身就是内容指纹,读取时可即时验证

字体数据条目使用 store(不压缩):字体文件本身已是二进制格式,压缩收益有限, 而 store 让随机访问与增量追加更直接。

关键约束 ​

约束内容
偏移修正前置 32 字节 Header 后,必须同步修正 ZIP 结构中所有偏移字段
中央目录位置必须位于数据区之后、EOCD 之前
Footer 位置Footer Descriptor 必须紧跟 ZIP EOCD
字节序Header 与 Footer 中所有多字节整数必须小端序
条目命名字体条目以内容 SHA-256 命名,实现天然去重

关于偏移修正 ​

这是实现时最容易出错的一步。ZIP 的权威索引是文件末尾的 EOCD, 其中记录的 offset of start of central directory 是相对文件起始的绝对偏移; 在前面插入 32 字节后,必须把偏移字段整体后移 32:

  • EOCD 中的中央目录偏移
  • 每一个中央目录条目中的 Local File Header 偏移
python
def patch_offsets(raw, delta):
    """把 ZIP 结构中的偏移字段整体后移 delta。"""
    b = bytearray(raw)
    eo = b.rfind(b"PK\x05\x06")                       # 定位 EOCD
    cd_off = struct.unpack_from("<I", b, eo + 16)[0]  # 中央目录偏移
    cd_size = struct.unpack_from("<I", b, eo + 12)[0] # 中央目录大小
    struct.pack_into("<I", b, eo + 16, cd_off + delta)

    pos = cd_off
    while pos < cd_off + cd_size and b[pos:pos + 4] == b"PK\x01\x02":
        name_len  = struct.unpack_from("<H", b, pos + 28)[0]
        extra_len = struct.unpack_from("<H", b, pos + 30)[0]
        cmt_len   = struct.unpack_from("<H", b, pos + 32)[0]
        lho = struct.unpack_from("<I", b, pos + 42)[0]
        struct.pack_into("<I", b, pos + 42, lho + delta)  # 每个条目一处
        pos += 46 + name_len + extra_len + cmt_len
    return bytes(b)

实测发现:多数工具能容忍未修正的偏移

7-Zip、bsdtar、Python zipfile 三者对未修正偏移的文件也能打开, 因为它们各自实现了前缀补偿逻辑(用实际位置反推前缀长度)。 但这不构成可以不修正的理由:

  • 这是各实现的宽容行为,不是 ZIP 规范的要求
  • 依赖宽容行为会掩盖真正的偏移错误,问题被推迟到特定条目解压时才暴露

因此规范中的"必须修正"依然正确,实现指南建议的"打包后用独立 ZIP 库自检"依然必要 —— 它是唯一能可靠发现偏移漏改的手段。

尾部结构与尺寸边界 ​

Footer Descriptor(24 字节)位于 EOCD 之后,恢复记录位于 Footer 之前:

[...ZIP 数据...][Central Directory][ZIP64 EOCD][Locator][EOCD][恢复记录][Footer Descriptor]
                                                                └── EOCD 之后的数据 ──┘

ZIP 规范要求 EOCD 是文件最后一个结构,其后只允许注释,且注释长度记录在 EOCD 的 commentLength 字段里(2 字节,最大 65535)。主流实现的定位策略就是只回扫文件末尾 64 KB。

于是产生一个隐含的尺寸上限:

recoveryLength + 24(Footer) <= 65535
  • 不超过:严格实现仍能在 64 KB 窗口内找到 EOCD,正常打开
  • 超过:EOCD 被推出搜索窗口,bsdtar 与 Python zipfile 会报"不是压缩包"

7-Zip 因搜索范围更宽而不受此限制。完整的实测数据与修复建议见 通用工具兼容性。

相关 ​

本仓库是协议层规范仓库,规范文本以 MIT 发布;包内字体的再分发受其自身许可证约束。