Go:不用标准库如何解压 zip 文件?

zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip 。
首先看看 zip 文件是如何工作的 。以一个小文件为例:(类 Unix 系统下)
$ cat hello.textHello!执行 zip 命令进行归档:
$ zip test.zip hello.textadding: hello.text (stored 0%)$ ls -lah test.zip-rw-r--r-- 1 phil phil 177 Nov 23 23:04 test.zip一个 6 字节的文本文件变成了一个 177 字节的 zip 文件 。这并不大,解析 177 个字节听起来不可能太复杂!
对 zip 文件执行 hexdump:
$ hexdump -C test.zip0000000050 4b 03 04 0a 00 00 0000 00 8a b8 77 53 9e d8|PK..........wS..|0000001042 b0 07 00 00 00 07 0000 00 0a 00 1c 00 68 65|B.............he|000000206c 6c 6f 2e 74 65 78 7455 54 09 00 03 74 73 9d|llo.textUT...ts.|0000003061 74 73 9d 61 75 78 0b00 01 04 eb 03 00 00 04|ats.aux.........|00000040eb 03 00 00 48 65 6c 6c6f 21 0a 50 4b 01 02 1e|....Hello!.PK...|0000005003 0a 00 00 00 00 00 8ab8 77 53 9e d8 42 b0 07|.........wS..B..|0000006000 00 00 07 00 00 00 0a00 18 00 00 00 00 00 01|................|0000007000 00 00 a4 81 00 00 0000 68 65 6c 6c 6f 2e 74|.........hello.t|0000008065 78 74 55 54 05 00 0374 73 9d 61 75 78 0b 00|extUT...ts.aux..|0000009001 04 eb 03 00 00 04 eb03 00 00 50 4b 05 06 00|...........PK...|000000a000 00 00 01 00 01 00 5000 00 00 4b 00 00 00 00|.......P...K....|000000b000|.|000000b1从中我们可以看到文件名和文件内容 。
01 结构我们来看看这里[1]定义的 zip 结构。根据第 4.3.6 节,看起来文件元数据后跟文件内容一个接一个地存储,最后一块是 “central directory” 元数据 。

Go:不用标准库如何解压 zip 文件?

文章插图
 
zip format header
图片来源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive
本地 header 元数据如下所示:
字段大小local file header signature4 bytesversion needed to extract2 bytesgeneral purpose bit flag2 bytescompression method2 byteslast mod file time2 byteslast mod file date2 bytescrc-324 bytescompressed size4 bytesuncompressed size4 bytesfile name length2 bytesextra field length2 bytesfile name可变extra field可变
在一个有效 zip 文件中,header 签名是一个整数 (0x04034b50 ) 。我们将忽略版本、通用 flag 和校验和 。可以是没有压缩(用 0 表示),也可以是使用 DEFLATE 方法解压缩(用 8 表示) 。
最后修改时间和日期是 MSDOS 风格的日期/时间格式 。
我们粗略地将其翻译为 Go 代码:
package mainimport ("os""bytes""compress/flate""io/ioutil""encoding/binary""time""fmt")type compression uint8const (noCompression compression = iotadeflateCompression)type localFileHeader struct {signature uint32version uint16bitFlag uint16compression compressionlastModified time.Timecrc32 uint32compressedSize uint32uncompressedSize uint32fileName stringextraField []bytefileContents string}02 main 函数实现我们的入口点将读取一个 zip 文件并遍历该文件,直到我们无法解析 zip 文件条目 。
func main() {f, err := ioutil.ReadFile(os.Args[1])if err != nil {panic(err)}end := 0for end < len(f) {var err errorvar lfh *localFileHeadervar next intlfh, next, err = parseLocalFileHeader(f, end)if err == errNotZip && end > 0 {break}if err != nil {panic(err)}end = nextfmt.Println(lfh.lastModified, lfh.fileName, lfh.fileContents)}}03 文件对于每个文件,如果前四个字节不是魔术 zip 签名(即 0x04034b50),则报错 。
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}基本模式是读取辅助函数将获取一个偏移量并返回一个 Go 值和一个新的偏移量 。读取辅助函数将进行边界检查 。
遵循相同的模式直到结构体的末尾:
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 := noCompressioncompressionRaw, 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}


推荐阅读