Go:不用標(biāo)準(zhǔn)庫如何解壓 zip 文件?
zip 是一種常見的歸檔格式,本文講解 Go 如何操作 zip。
首先看看 zip 文件是如何工作的。以一個小文件為例:(類 Unix 系統(tǒng)下)
$?cat?hello.text
Hello!
執(zhí)行 zip 命令進(jìn)行歸檔:
$?zip?test.zip?hello.text
adding:?hello.text?(stored?0%)
$?ls?-lah?test.zip
-rw-r--r--?1?phil?phil?177?Nov?23?23:04?test.zip
一個 6 字節(jié)的文本文件變成了一個 177 字節(jié)的 zip 文件。這并不大,解析 177 個字節(jié)聽起來不可能太復(fù)雜!
對 zip 文件執(zhí)行 hexdump:
$?hexdump?-C?test.zip
00000000??50?4b?03?04?0a?00?00?00??00?00?8a?b8?77?53?9e?d8??|PK..........wS..|
00000010??42?b0?07?00?00?00?07?00??00?00?0a?00?1c?00?68?65??|B.............he|
00000020??6c?6c?6f?2e?74?65?78?74??55?54?09?00?03?74?73?9d??|llo.textUT...ts.|
00000030??61?74?73?9d?61?75?78?0b??00?01?04?eb?03?00?00?04??|ats.aux.........|
00000040??eb?03?00?00?48?65?6c?6c??6f?21?0a?50?4b?01?02?1e??|....Hello!.PK...|
00000050??03?0a?00?00?00?00?00?8a??b8?77?53?9e?d8?42?b0?07??|.........wS..B..|
00000060??00?00?00?07?00?00?00?0a??00?18?00?00?00?00?00?01??|................|
00000070??00?00?00?a4?81?00?00?00??00?68?65?6c?6c?6f?2e?74??|.........hello.t|
00000080??65?78?74?55?54?05?00?03??74?73?9d?61?75?78?0b?00??|extUT...ts.aux..|
00000090??01?04?eb?03?00?00?04?eb??03?00?00?50?4b?05?06?00??|...........PK...|
000000a0??00?00?00?01?00?01?00?50??00?00?00?4b?00?00?00?00??|.......P...K....|
000000b0??00????????????????????????????????????????????????|.|
000000b1
從中我們可以看到文件名和文件內(nèi)容。
01 結(jié)構(gòu)
我們來看看這里[1]定義的 zip 結(jié)構(gòu) 。根據(jù)第 4.3.6 節(jié),看起來文件元數(shù)據(jù)后跟文件內(nèi)容一個接一個地存儲,最后一塊是 “central directory” 元數(shù)據(jù)。

圖片來源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive
本地 header 元數(shù)據(jù)如下所示:
| 字段 | 大小 |
|---|---|
| local file header signature | 4 bytes |
| version needed to extract | 2 bytes |
| general purpose bit flag | 2 bytes |
| compression method | 2 bytes |
| last mod file time | 2 bytes |
| last mod file date | 2 bytes |
| crc-32 | 4 bytes |
| compressed size | 4 bytes |
| uncompressed size | 4 bytes |
| file name length | 2 bytes |
| extra field length | 2 bytes |
| file name | 可變 |
| extra field | 可變 |
在一個有效 zip 文件中,header 簽名是一個整數(shù) (0x04034b50 )。我們將忽略版本、通用 flag 和校驗(yàn)和??梢允菦]有壓縮(用 0 表示),也可以是使用 DEFLATE ?方法解壓縮(用 8 表示)。
最后修改時間和日期是 MSDOS 風(fēng)格的日期/時間格式。
我們粗略地將其翻譯為 Go 代碼:
package?main
import?(
????"os"
????"bytes"
????"compress/flate"
????"io/ioutil"
????"encoding/binary"
????"time"
????"fmt"
)
type?compression?uint8
const?(
????noCompression?compression?=?iota
????deflateCompression
)
type?localFileHeader?struct?{
????signature?uint32
????version?uint16
????bitFlag?uint16
????compression?compression
????lastModified?time.Time
????crc32?uint32
????compressedSize?uint32
????uncompressedSize?uint32
????fileName?string
????extraField?[]byte
????fileContents?string
}
02 main 函數(shù)實(shí)現(xiàn)
我們的入口點(diǎn)將讀取一個 zip 文件并遍歷該文件,直到我們無法解析 zip 文件條目。
func?main()?{
????f,?err?:=?ioutil.ReadFile(os.Args[1])
????if?err?!=?nil?{
????????panic(err)
????}
????end?:=?0
????for?end?len(f)?{
????????var?err?error
????????var?lfh?*localFileHeader
????????var?next?int
????????lfh,?next,?err?=?parseLocalFileHeader(f,?end)
????????if?err?==?errNotZip?&&?end?>?0?{
????????????break
????????}
????????if?err?!=?nil?{
????????????panic(err)
????????}
????????end?=?next
????????fmt.Println(lfh.lastModified,?lfh.fileName,?lfh.fileContents)
????}
}
03 文件
對于每個文件,如果前四個字節(jié)不是魔術(shù) zip 簽名(即 0x04034b50),則報(bào)錯。
var?errNotZip?=?fmt.Errorf("Not?a?zip?file")
func?parseLocalFileHeader(bs?[]byte,?start?int)?(*localFileHeader,?int,?error)?{
????signature,?i,?err?:=?readUint32(bs,?start)
????if?signature?!=?0x04034b50?{
????????return?nil,?0,?errNotZip
????}
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
基本模式是讀取輔助函數(shù)將獲取一個偏移量并返回一個 Go 值和一個新的偏移量。讀取輔助函數(shù)將進(jìn)行邊界檢查。
遵循相同的模式直到結(jié)構(gòu)體的末尾:
????version,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????bitFlag,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????compression?:=?noCompression
????compressionRaw,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????if?compressionRaw?==?8?{
????????compression?=?deflateCompression
????}
????lmTime,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????lmDate,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????lastModified?:=?msdosTimeToGoTime(lmDate,?lmTime)
????crc32,?i,?err?:=?readUint32(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????compressedSize,?i,?err?:=?readUint32(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????uncompressedSize,?i,?err?:=?readUint32(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????fileNameLength,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????extraFieldLength,?i,?err?:=?readUint16(bs,?i)
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????fileName,?i,?err?:=?readString(bs,?i,?int(fileNameLength))
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
????extraField,?i,?err?:=?readBytes(bs,?i,?int(extraFieldLength))
????if?err?!=?nil?{
????????return?nil,?0,?err
????}
現(xiàn)在,如果文件內(nèi)容未壓縮,我們只需復(fù)制文件頭后的字節(jié)即可。如果文件內(nèi)容被壓縮,我們將使用 Go 的內(nèi)置 DEFLATE 支持來解壓縮文件頭之后的字節(jié)。
????var?fileContents?string
????if?compression?==?noCompression?{
????????fileContents,?i,?err?=?readString(bs,?i,?int(uncompressedSize))
????????if?err?!=?nil?{
????????????return?nil,?0,?err
????????}
????}?else?{
????????end?:=?i?+?int(compressedSize)
????????if?end?>?len(bs)?{
????????????return?nil,?0,?errOverranBuffer
????????}
????????flateReader?:=?flate.NewReader(bytes.NewReader(bs[i:end]))
????????defer?flateReader.Close()
????????read,?err?:=?ioutil.ReadAll(flateReader)
????????if?err?!=?nil?{
????????????return?nil,?0,?err
????????}
????????fileContents?=?string(read)
????????i?=?end
????}
并返回填充好的結(jié)構(gòu)體實(shí)例:
????return?&localFileHeader{
????????signature:?signature,
????????version:?version,
????????bitFlag:?bitFlag,
????????compression:?compression,
????????lastModified:?lastModified,
????????crc32:?crc32,
????????compressedSize:?compressedSize,
????????uncompressedSize:?uncompressedSize,
????????fileName:?fileName,
????????extraField:?extraField,
????????fileContents:?fileContents,
????},?i,?nil
}
04 讀取輔助函數(shù)
現(xiàn)在我們只定義那些帶有邊界檢查的讀取輔助函數(shù),使用 Go 的內(nèi)置庫來處理二進(jìn)制編碼。
var?errOverranBuffer?=?fmt.Errorf("Overran?buffer")
func?readUint32(bs?[]byte,?offset?int)?(uint32,?int,?error)?{
????end?:=?offset?+?4
????if?end?>?len(bs)?{
????????return?0,?0,?errOverranBuffer
????}
????return?binary.LittleEndian.Uint32(bs[offset:end]),?end,?nil
}
func?readUint16(bs?[]byte,?offset?int)?(uint16,?int,?error)?{
????end?:=?offset+2
????if?end?>?len(bs)?{
????????return?0,?0,?errOverranBuffer
????}
????return?binary.LittleEndian.Uint16(bs[offset:end]),?end,?nil
}
并且基本上只對獲取的字節(jié)和字符串進(jìn)行邊界檢查。
func?readBytes(bs?[]byte,?offset?int,?n?int)?([]byte,?int,?error)?{
????end?:=?offset?+?n
????if?end?>?len(bs)?{
????????return?nil,?0,?errOverranBuffer
????}
????return?bs[offset:offset+n],?end,?nil
}
func?readString(bs?[]byte,?offset?int,?n?int)?(string,?int,?error)?{
????read,?end,?err?:=?readBytes(bs,?offset,?n)
????return?string(read),?end,?err
}
05 MSDOS 時間
我猜在創(chuàng)建 zip 時,MSDOS 時間格式很流行。但它在今天并不流行,所以花了一些時間才最終用一些代碼(模仿 C 語言)找到對該格式的解釋[2]。
func?msdosTimeToGoTime(d?uint16,?t?uint16)?time.Time?{
????seconds?:=?int((t?&?0x1F)?*?2)
????minutes?:=?int((t?>>?5)?&?0x3F)
????hours?:=?int(t?>>?11)
????day?:=?int(d?&?0x1F)
????month?:=?time.Month((d?>>?5)?&?0x0F)
????year?:=?int((d?>>?9)?&?0x7F)?+?1980
????return?time.Date(year,?month,?day,?hours,?minutes,?seconds,?0,?time.Local)
}
06 測試
運(yùn)行:
$?go?build
$?./gozip?test.zip
2021-11-23?23:04:20?+0000?UTC?hello.text?Hello!
這看起來不錯!現(xiàn)在讓我們嘗試壓縮多個文件。
$?cat?bye.text
Au?revoir!
$?rm?test.zip
$?zip?test.zip?*.text
??adding:?bye.text?(stored?0%)
??adding:?hello.text?(stored?0%)
$?./gozip?test.zip
2021-11-24?03:40:00?+0000?UTC?bye.text?Au?revoir!
2021-11-23?23:04:20?+0000?UTC?hello.text?Hello!
一切正常。
07 總結(jié)
實(shí)際上,還有許多標(biāo)準(zhǔn)需要處理(例如目錄)和許多常見的擴(kuò)展,本文沒有涉及。
文件末尾還有一些空間,這可能是 “central directory” 元數(shù)據(jù),但我還沒有深入研究。如果你有興趣可以查閱相關(guān)資料了解最后剩下的部分內(nèi)容。
原文鏈接:https://notes.eatonphil.com/implementing-zip-in-go-unzipping.html
參考資料
這里: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
[2]對該格式的解釋: https://groups.google.com/g/comp.os.msdos.programmer/c/ffAVUFN2NbA
推薦閱讀
