您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

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

时间:2021-12-17 10:23:58  来源:  作者:Go语言中文网

zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。

首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)

$ cat hello.text
Hello!

执行 zip 命令进行归档:

$ 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 字节的文本文件变成了一个 177 字节的 zip 文件。这并不大,解析 177 个字节听起来不可能太复杂!

对 zip 文件执行 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

从中我们可以看到文件名和文件内容。

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 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 函数实现

我们的入口点将读取一个 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 文件

对于每个文件,如果前四个字节不是魔术 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 := 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
    }

现在,如果文件内容未压缩,我们只需复制文件头后的字节即可。如果文件内容被压缩,我们将使用 Go 的内置 DEFLATE 支持来解压缩文件头之后的字节。

    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
    }

并返回填充好的结构体实例:

    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 读取辅助函数

现在我们只定义那些带有边界检查的读取辅助函数,使用 Go 的内置库来处理二进制编码。

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
}

并且基本上只对获取的字节和字符串进行边界检查。

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 时间

我猜在创建 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 测试

运行:

$ go build
$ ./gozip test.zip
2021-11-23 23:04:20 +0000 UTC hello.text Hello!

这看起来不错!现在让我们尝试压缩多个文件。

$ 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 总结

实际上,还有许多标准需要处理(例如目录)和许多常见的扩展,本文没有涉及。

文件末尾还有一些空间,这可能是 “central directory” 元数据,但我还没有深入研究。如果你有兴趣可以查阅相关资料了解最后剩下的部分内容。

原文链接:https://notes.eatonphil.com/implementing-zip-in-go-unzipping.html

参考资料

[1]

这里: https://pkware.cachefly.net/webdocs/casestudies/AppNOTE.TXT

[2]

对该格式的解释: https://groups.google.com/g/comp.os.msdos.programmer/c/ffAVUFN2NbA



Tags:Go语言   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)$ cat hello.textHello!执行 zip 命令进行归档:$ zip...【详细内容】
2021-12-17  Tags: Go语言  点击:(12)  评论:(0)  加入收藏
统一规范篇合理规划目录本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制...【详细内容】
2021-05-18  Tags: Go语言  点击:(232)  评论:(0)  加入收藏
闭包概述 闭包不是Go语言独有的概念,在很多编程语言中都有闭包 闭包就是解决局部变量不能被外部访问的一种解决方案 是把函数当作返回值的一种应用 代码演示总体思想:在函数...【详细内容】
2021-05-14  Tags: Go语言  点击:(223)  评论:(0)  加入收藏
一时想不开,想了解一下Go语言,于是安装了并体验了一下。下载1. 进入golang.google.cn 点击Download Go 2.选择对应的操作系统,点击后开始下载。 安装1. windows下执行傻瓜式安...【详细内容】
2021-05-12  Tags: Go语言  点击:(236)  评论:(0)  加入收藏
再次整理了一下这个日志收集系统的框,如下图 这次要实现的代码的整体逻辑为: 完整代码地址为: https://github.com/pythonsite/logagentetcd介绍高可用的分布式key-value存储,...【详细内容】
2021-01-14  Tags: Go语言  点击:(135)  评论:(0)  加入收藏
生命不止,继续 Go go go !!!之前关于golang操作数据库的博客:Go实战&ndash;go语言操作MySQL数据库(go-sql-driver/mysql)Go实战&ndash;go语言操作sqlite数据库(The way to go)...【详细内容】
2021-01-05  Tags: Go语言  点击:(196)  评论:(0)  加入收藏
欢迎访问我的GitHubhttps://github.com/zq2599/blog_demos内容:所有原创文章分类和汇总,及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;关于《gRPC学习》系列《gRPC学习》...【详细内容】
2020-12-14  Tags: Go语言  点击:(124)  评论:(0)  加入收藏
概述Go语言作为一门开源的编程语言,以简洁、快速、安全著称。尤其在高性能的分布式服务器领域得到广泛应用。技多不压身,在学习过程中记录下来,以备后续参考,希望对有同样需求的...【详细内容】
2020-11-12  Tags: Go语言  点击:(65)  评论:(0)  加入收藏
go-fly基于GO语言实现的web客服即时通讯与客服管理系统。非常适合给自己的网站增加在线客服功能,代码简单也适合学习。Github地址:https://github.com/taoshihan1991/go-fly1....【详细内容】
2020-09-25  Tags: Go语言  点击:(154)  评论:(0)  加入收藏
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee Fℹ️ 这篇文章基于 Go 1.13。在内存从分配到回收的生命周期中,内存...【详细内容】
2020-09-24  Tags: Go语言  点击:(65)  评论:(0)  加入收藏
▌简易百科推荐
zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)$ cat hello.textHello!执行 zip 命令进行归档:$ zip...【详细内容】
2021-12-17  Go语言中文网    Tags:Go语言   点击:(12)  评论:(0)  加入收藏
大家好,我是 polarisxu。前段时间,Russ Cox 明确了泛型相关的事情,原计划在标准库中加入泛型相关的包,改放到 golang.org/x/exp 下。目前,Go 泛型的主要设计者 ianlancetaylor 完...【详细内容】
2021-11-30  Go语言中文网    Tags:slices 包   点击:(24)  评论:(0)  加入收藏
前言最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼”真香“。 但现阶段相对来说还是 Python 写的多一些,偶尔还...【详细内容】
2021-11-25  crossoverJie    Tags:Go   点击:(29)  评论:(0)  加入收藏
go-micro是基于 Go 语言用于开发的微服务的 RPC 框架,主要功能如下:服务发现,负载均衡 ,消息编码,请求/响应,Async Messaging,可插拔接口,最后这个功能牛p安装步骤安装proto...【详细内容】
2021-09-06    石老师小跟班  Tags:go-micro   点击:(196)  评论:(0)  加入收藏
GoLand 2021.2 EAP 5 现已发布。用户可以从工具箱应用程序中获得 EAP 构建,也可以从官方网站手动下载。并且从此 EAP 开始,只有拥有有效的 JetBrains 帐户才能加入该计划。手...【详细内容】
2021-06-29  IT实战联盟  今日头条  Tags:GoLand   点击:(185)  评论:(0)  加入收藏
作者:HDT3213今天给大家带来的开源项目是 Godis:一个用 Go 语言实现的 Redis 服务器。支持: 5 种数据结构(string、list、hash、set、sortedset) 自动过期(TTL) 发布订阅、地理位...【详细内容】
2021-06-18  HelloGitHub  今日头条  Tags:Go   点击:(125)  评论:(0)  加入收藏
统一规范篇合理规划目录本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制...【详细内容】
2021-05-18  1024课堂    Tags:Go语言   点击:(232)  评论:(0)  加入收藏
闭包概述 闭包不是Go语言独有的概念,在很多编程语言中都有闭包 闭包就是解决局部变量不能被外部访问的一种解决方案 是把函数当作返回值的一种应用 代码演示总体思想:在函数...【详细内容】
2021-05-14  HelloGo  今日头条  Tags:Go语言   点击:(223)  评论:(0)  加入收藏
一时想不开,想了解一下Go语言,于是安装了并体验了一下。下载1. 进入golang.google.cn 点击Download Go 2.选择对应的操作系统,点击后开始下载。 安装1. windows下执行傻瓜式安...【详细内容】
2021-05-12  程序员fearlazy  fearlazy  Tags:Go语言   点击:(236)  评论:(0)  加入收藏
1.简介channel是Go语言的一大特性,基于channel有很多值得探讨的问题,如 channel为什么是并发安全的? 同步通道和异步通道有啥区别? 通道为何会阻塞协程? 使用通道导致阻塞的协程...【详细内容】
2021-05-10  程序员麻辣烫  今日头条  Tags:Go通道   点击:(272)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条