KernelRanch
返回文章列表

内容一样,gzip 文件的哈希为什么变了?检查时间戳和原始文件名

2026-11-13 #gzip#python#reproducible-builds

比较两份 gzip 产物时,先分清需要稳定的是压缩包字节还是解压内容。gzip 头部可以带时间戳和原始文件名,输入完全一致时,这些字段也会改变产物哈希。用 Python 生成可复现的 gzip 文件时,可明确指定 mtime=0 和 filename=""。

1. 每次只改变一个字段

将以下代码保存为 gzip_header_lab.py,用 Python 运行。样本固定输入和压缩级别,使用内存文件观察 gzip 字节。

from hashlib import sha256
import gzip
from io import BytesIO

payload = b"build artifact\n" * 20


def compress(filename, mtime):
    output = BytesIO()
    with gzip.GzipFile(filename=filename, fileobj=output, mode="wb",
                       compresslevel=6, mtime=mtime) as stream:
        stream.write(payload)
    return output.getvalue()


baseline = compress("", 0)
for label, data, same in [
    ("timestamp", compress("", 1700000000), False),
    ("filename", compress("report.txt", 0), False),
    ("fixed metadata", compress("", 0), True),
]:
    mtime = int.from_bytes(data[4:8], "little")
    has_filename = bool(data[3] & 0x08)
    same_hash = sha256(baseline).digest() == sha256(data).digest()
    assert gzip.decompress(data) == payload
    assert same_hash == same
    print(f"{label}: mtime={mtime} fname={has_filename} same_sha256={same_hash}")
timestamp: mtime=1700000000 fname=False same_sha256=False
filename: mtime=0 fname=True same_sha256=False
fixed metadata: mtime=0 fname=False same_sha256=True

2. 找出真正需要稳定的构建层

gzip 固定头部从偏移 4 开始存放四字节 MTIME,偏移 3 是标志字节。设置 FNAME 位表示头部随后带有以零字节结束的原始文件名。一般排查可以先对两份压缩包做解压并比较内容哈希,再检查头部。如果解压结果已经不同,应回到生成输入的阶段查换行、编码或记录顺序。

固定这两个字段以后,同一工具链内仍应在干净目录重复构建,比较整份产物的 SHA-256。跨环境发布时还应固定压缩器版本和压缩级别。若 gzip 包裹的是 tar,内部条目名、顺序、权限和时间也可能改变待压缩字节,需在打包阶段另行控制。

小结

gzip 有自己的元数据。固定头部字段后,再用产物哈希验证构建是否可复现。

数据来源与检索时间

来源URL检索日期对应内容
RFC 1952 §2.3.1https://www.rfc-editor.org/rfc/rfc1952.html#section-2.3.12026-11-13gzip 成员头部的 MTIME 和 FNAME
Python gzip.GzipFilehttps://docs.python.org/3.11/library/gzip.html#gzip.GzipFile2026-11-13filename、mtime 与压缩级别参数

核验说明

  • Windows、Python 3.11.9 对三个 gzip 样本分别核对头部、解压内容和 SHA-256。跨工具链字节一致性需在实际构建环境验证。