4. 一个 DEFLATE 主体,三条格式路径
d5c1fe8e93657430 · 对应提交的原文 ↗裸 DEFLATE(raw DEFLATE)没有额外包装;zlib 和 gzip 在 DEFLATE 主体外添加头部和尾部。三者可以为 ABC 携带相同的主体。
不同的头部和尾部建立解码起始条件,并验证结果。这样就能把共享的解码论证与各包装格式的要求区分开。
目录
- 4.1 把同一个主体放入三种包装
- 4.2 头部建立解码器的起始条件
- 4.2.1 窗口与可选的初始历史
- 4.2.2 Gzip 元数据有独立的接收位置
- 4.3 沿着字节追踪校验和
- 4.4 输出可以先于检查失败发生
- 4.5 结束一个成员,再开始下一个
4.1 把同一个主体放入三种包装
一个最终存储块可以不使用 Huffman 编码,直接携带这三个字节:
01 | 03 00 | fc ff | 41 42 43
LEN=3 NLEN A B C
01 包含最终块标志、存储块类型和填充位;03 00 是长度;fc ff 是它的 16 位反码。
这份选定的编码不一定是压缩器的选择。编码一章解释其他选择。
图 4.1。蓝色表示共享主体,灰色表示头部,琥珀色表示尾部。宽度是示意性的, 十六进制值是实际字节。颜色表示数据角色,不表示状态。
三条路径必须对这个块作出相同解释。包装格式的证明定位主体边界,确保尾部既不变成输出,也不被解释为下一个块。 格式规则定义字节含义; 内存边界论证则分别证明每次输入读取有效。
4.2 头部建立解码器的起始条件
各路径共享块解码模式,但历史和窗口参数可能不同。
flowchart TD
select["配置的输入格式"] --> raw["裸流:没有包装头"]
select --> zh["zlib 头:方法、窗口、字典标志"]
select --> gh["gzip 头:方法、标志、可选元数据"]
zh -->|"请求字典"| dict["检查标识,提供初始历史"]
zh -->|"没有字典"| body["共享的 DEFLATE 块解码"]
dict --> body
gh --> body
raw --> body
body -->|"裸流"| done["完成"]
body -->|"zlib 封装"| za["检查 Adler-32"]
body -->|"gzip 封装"| gc["检查 CRC-32 和模 2^32 的长度"]
za --> done
gc --> done
图 4.2。这是普通流程的概念路径,不表示各自独立的 C 函数。 头部和尾部都可以跨越多次调用;格式错误的字段会转入错误路径。
在 inflate() 内部,HEAD 和 gzip 模式解析头部;TYPE、表和符号模式解码主体;
CHECK 与 LENGTH 处理尾部。完整的头部、主体和尾部组合证明未完成;仍需按实际模式转换建立这些联系。
辅助函数分别负责表(inflate_table())、快速片段(inflate_fast())和历史(updatewindow()),
没有哪个辅助函数包含整个核心。模式证明沿着这些边界展开,无需重写翻译。
4.2.1 窗口与可选的初始历史
窗口大小在这里按字节计,1 KiB 为 1024 字节。如果 zlib 头声明 32 KiB 窗口,而解码器最多允许 16 KiB,就必须拒绝这个头部。
若请求采用头部指定的窗口,则从头部取得大小。
INFLATE_STRICT 还会把头部的最大距离用于主体检查;这一选项不属于最初的默认构建。
如果两端都以末尾为 AB 的字典开始,距离为二的引用可以在当前流尚未输出两个字节前使用这段历史。
没有初始历史时,同样的引用就没有依据。
因此,核心命题要带上有效的初始历史和窗口条件,由实际的头部、字典操作建立。 Adler-32 字典标识可能碰撞:恢复预期内容需要匹配的字节,不能仅凭相等的标识。 精确范围见窗口请求和 字典条件; 状态操作一章解释 raw 字典。
4.2.2 Gzip 元数据有独立的接收位置
Gzip 的名称、注释和额外字段不会改变字面量、匹配或 Huffman 规则。 Gzip 没有 zlib 式的预置字典请求。
inflateGetHeader() 登记调用者拥有的接收位置;后续 inflate() 才解析并写入。
名称跨越输入缓冲区时,需要保存解析进度、遵守容量边界,并确保借用的存储在调用之间仍然有效。
头部契约同时定义这些内存效果和字节含义。
可选的头部检查保存先前头部字节的 CRC-32 低 16 位。 载荷 CRC 则从未压缩数据重新开始累计。即使 CRC 函数正确,混淆这两段历史也会拒绝有效输入。
4.3 沿着字节追踪校验和
先看为什么同一份数据分批处理也能得到同一个校验值。Adler-32 保存两个累加量:s1 累积字节值,s2 再累积每一步更新后的 s1。对于 ABC(字节数值为 65、66、67),从 s1 = 1、s2 = 0 开始。
逐字节把字节值加到 s1,再把 s1 加到 s2,都对 65521 取模:
| 已处理前缀 | s1 |
s2 |
|---|---|---|
| 空 | 1 | 0 |
A |
66 | 66 |
AB |
132 | 198 |
ABC |
199 | 397 |
结果是 397 * 65536 + 199 = 0x018d00c7;zlib 按最高有效字节在前的顺序写出。
CRC-32 是 0xa3830348;gzip 按最低有效字节在前的顺序写出 CRC 和长度(图 4.1)。
这些计算示范了独立的校验和定义,不是实现证明。
flowchart LR
body["共享主体解码器"] --> a["输出 A"]
a --> bc["输出 BC"]
initial["空载荷的校验和"] --> ca["用 A 更新"]
a -.->|"恰好是这些字节"| ca
ca --> cabc["用 BC 更新"]
bc -.->|"恰好是这些字节"| cabc
cabc --> compare["与尾部比较"]
图 4.3。解压对已输出字节计算校验和;压缩对已接受输入计算校验和。 两者都不把压缩后的字节当作载荷来计算校验和。
先用 A 更新、再用 BC 更新,必须等于用 ABC 更新。
不变量要求累加值恰好等于已处理前缀的校验和:不能跳过、重复,也不能混入头部字节。
Adler-32和 CRC-32论证算术与读取;
调用者的证明再把那些实际输入范围连接到主体证明中的输出前缀。
4.4 输出可以先于检查失败发生
先提供图 4.1 中 zlib 格式的头部和主体,不提供四个尾部字节。
输出 ABC 后,再提供 01 8d 00 c6,而非正确的 01 8d 00 c7。
sequenceDiagram
participant App as 应用
participant Dec as 解码器
App->>Dec: ABC 的头部和存储块
Dec-->>App: 输出 ABC,当前流尚未完成
App->>Dec: 尾部 01 8d 00 c6
Dec-->>App: Z_DATA_ERROR:校验和不匹配
图 4.4。已经输出的字节不能撤回;完整流的验证发生在后面。
普通验证必须安全地拒绝不匹配,并允许清理。 校验和相等本身既不能证明字节相等,也不能证明真实性;还需要主体解码关系。 不完整的尾部则需要继续输入。详见结果契约和 尾部证明。
4.5 结束一个成员,再开始下一个
在 member("ABC") || member("DEF") 中,|| 表示拼接。
每个 gzip 成员都有自己的头部、主体、校验和和长度。
inflate() 在第一个 Z_STREAM_END 处停止;处理下一个成员前,重置活跃解码器,并保留未读输入。
第一个成员:头部 → 主体 ABC → CRC/长度 → Z_STREAM_END
│ 重置
下一个成员:头部 → 主体 DEF → CRC/长度 → Z_STREAM_END
组合证明必须按实际成员终点划分输入,重新开始历史和校验和,并把输出拼成 ABCDEF。
在第一个终点就停止会丢失 DEF。详见重置操作;
gzip 文件读取接口会管理连续成员。
共享的范围与证据规则适用于本章全部内容。