GZIP 檔案格式
gzip 壓縮的資料,通常包的是單一檔案或一個 tar 封存檔。
- MIME 類型
- application/gzip
- 副檔名
- .gz
- 分類
- 壓縮與封存
Magic number(檔案簽章)
| 0x00000000 | 1F8B | ·· | gzip magic |
關於 GZIP 格式
gzip 用 DEFLATE 壓縮單一資料流,是 HTTP 壓縮的支柱,也是到處都看得到的 .tar.gz 的真面目。檔案以 2 個 magic 位元組 1F 8B 開頭。這個格式刻意做得很小 — 十個位元組的固定標頭、幾個選用欄位、deflate 串流,加上八個位元組的結尾 — 也刻意可以串流:開始壓縮之前不需要先知道任何東西,結束時也不需要倒回去補任何東西。而這同時就是它的限制。它沒有目錄、沒有逐檔結構,也沒有辦法在不解壓縮前面所有資料的情況下讀到中間。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 2 | Magic ID1 ID2 | 1F 8B。兩個位元組都不是可列印的 ASCII,這是刻意的:gzip 串流絕不能讓郵件閘道或某個想幫忙的終端機看起來像文字。 |
| 2 | 1 | Compression method (CM) | 8,代表 deflate。0 到 7 是保留值,從來沒有一個被指派過,所以實務上這裡只要不是 08,不是這個檔案不是 gzip,就是標頭壞了。 |
| 3 | 1 | Flags (FLG) | 位元 0 FTEXT(內容很可能是文字)、位元 1 FHCRC(後面跟著兩個位元組的標頭 CRC)、位元 2 FEXTRA、位元 3 FNAME(後面跟著一個以 NUL 結尾的原始檔名)、位元 4 FCOMMENT。用命令列工具壓出來的檔案通常讀到 08,因為它會存檔名。 |
| 4 | 4 | Modification time (MTIME) | 原始檔案的 Unix 秒數,小端序,取不到時就是 0。同樣的輸入壓兩次會得到不同的位元組就是因為它,而可重現建置也是為此加旗標把它歸零。 |
| 8 | 1 | Extra flags (XFL) | 配上 deflate 時,2 代表用了最慢也最好的壓縮,4 代表用了最快的;其他情況是 0。它純粹是參考用 — 沒有任何解壓縮器會拿它做事。 |
| 9 | 1 | Operating system (OS) | 原始檔案來自哪種檔案系統:0 是 FAT、3 是 Unix、11 是 NTFS、255 是未知。它會洩漏檔案是在哪裡建置的,這也是可重現建置會去正規化的另一個欄位。 |
| 10 | variable | Optional fields, then the deflate stream | 被設起來的那些旗標對應的欄位,順序固定:額外欄位(兩個位元組的長度加那麼多位元組)、原始檔名、註解、標頭 CRC-16。一個存著檔名 'payload.bin' 的檔案,位移 10 到 21 花在它身上,deflate 從 22 開始。 |
| -8 | 4 | CRC-32 of the uncompressed data | 小端序,算的是原始位元組、不是壓縮後的位元組。`gzip -t` 檢查的就是它,而被截斷的檔案永遠滿足不了它。 |
| -4 | 4 | ISIZE | 原始大小對 2^32 取模,小端序。200000 位元組的輸入結尾是 40 0D 03 00。因為它是 32 位元的,超過四 GB 就會悄悄繞回去 — 這就是為什麼 `gzip -l` 對超大檔案報出來的是胡說八道,也是為什麼絕對不能拿它當配置緩衝區的大小。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| zlib | 這一對才是真的會坑到人,因為兩者包的是同一條 deflate 串流。zlib 串流的標頭只有兩個位元組、不是十個 — 實務上是 78 01、78 9C 或 78 DA — 而且結尾是 Adler-32 而不是 CRC-32。它沒有 magic number、沒有檔名也沒有長度,所以一個被改名成 .gz 的 zlib 區塊哪裡都打不開,而把 gzip 餵給 zlib 解碼器則會當場失敗。 |
| raw deflate | RFC 1951 定義的裸 deflate 完全沒有標頭也沒有結尾 — 它直接從第一個區塊的三個位元開始。ZIP 項目存的就是它,而 HTTP 那個曖昧的 `Content-Encoding: deflate` 講的也是它 — 這也是為什麼那麼多 HTTP 用戶端會先試 zlib、失敗再退回裸的。 |
| TAR | 一個 .tar.gz 和「壓了一個普通檔案的 gzip」是同一種格式,差別只在內容。解壓縮之後看結果的位移 257 — tar 在那裡有 'ustar',位在第一個 512 位元組成員標頭裡面。更省事的是位移 10 的 FNAME 欄位,它通常會以 .tar 結尾,自己招了。 |
| ZIP | 1F 8B 對上 50 4B 03 04,而且結構上比「壓縮」這個共通詞暗示的差得更遠:ZIP 檔尾有一份中央目錄,可以在不碰其他部分的情況下列出並抽出單一項目,而 gzip 串流要讀到任何東西都得從頭開始。 |
| BZIP2 | 'BZh' 加一個數字對上 1F 8B。兩者在 Unix 上的用法一模一樣,在建置腳本裡幾乎被當成同義詞,所以副檔名常常是錯的;前兩個位元組則從來不會錯。 |
常見的壞法
- 下載被截斷。八個位元組的結尾不見了,所以解壓縮器會把它解得出來的全部吐出來,然後回報非預期的檔案結束。救回來的位元組通常是好的 — 問題在於沒有東西能確認這件事,因為長度和檢查碼都住在那段消失的尾巴裡。
- deflate 串流裡有一個位元被翻掉。輸出長度是對的,而位移 -8 的 CRC-32 對不上,這正是 `gzip -t` 存在要指出的情況。看那個位元落在哪裡,解壓縮也可能根本走不到檢查碼那一步,就先因為不合法的 distance 或碼長錯誤而整個失敗。
- 文字模式傳輸。換行轉換把壓縮資料裡的 0D 0A 改寫成 0A,而十個位元組的標頭原封不動,所以這個檔案照樣被認成 gzip、照樣報得出原始檔名,然後在串流中途失敗。
- 多成員的封存檔被當成一個來讀。把 gzip 檔案串接起來是合法的,合規的解壓縮器會把它們全部接著吐出來,但一個自己手寫、讀到第一個 ISIZE 就停的剖析器,會靜靜地把第一個成員之後的東西全部丟掉。平行壓縮器產出的正是這種形狀。
- 把 ISIZE 當成大小來相信。超過四 GB 它就繞回去,所以拿它去配置緩衝區的程式,會少配整整幾個 4 GiB。它是一個描述資料的標頭欄位,不是一個承諾,而且沒有任何檢查碼保護它。
這個格式現在還在用嗎
gzip 是全世界被執行次數最多的壓縮格式,而且看不出有要退場的跡象。每個 HTTP 技術棧都還在協商它,.tar.gz 是 Unix 上原始碼發佈的預設形狀,log 輪替寫的是它,而每種語言的標準函式庫都內建一個解碼器 — 所以只要不確定讀取端是誰,它就是那個安全答案。壓縮率上它早就輸了:bzip2、xz 和 Zstandard 全都贏它,而在網頁上,靜態文字是 Brotli 贏,相近壓縮率下的速度則是 Zstandard 贏。讓 gzip 保住第一選擇的是通用性加上可串流:它不需要 seek、不需要暫存檔,也不需要事先知道輸入長度,所以在管線裡、在 socket 裡、在韌體映像檔裡都能用。而只要你需要抽出一堆檔案裡的某一個,它就是錯的選擇,因為它沒有索引 — 那是 ZIP 的工作。
在這個站上打開 GZIP 檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 GZIP 搞混的格式,可以比對它們的開頭位元組。