zip文件详解
zip 文件格式是一种常用的压缩和归档格式,用于将多个文件和目录打包到一个单独的文件中,同时对其内容进行压缩以减少文件大小。zip 文件格式的设计旨在支持多种压缩算法、加密和数据完整性校验。以下是 zip 文件格式的主要特性和常用算法:
zip 文件格式主要特性
文件头:
每个文件都有一个本地文件头和一个中央目录文件头。文件头包含文件名、压缩方法、时间戳、crc-32 校验和、压缩前后的大小等信息。
数据描述符:
可选的后缀结构,包含文件的 crc-32 校验和、压缩大小和未压缩大小。
中央目录:
zip 文件的末尾包含一个中央目录记录,列出了 zip 文件中的所有文件和目录的文件头信息,用于快速定位和访问。
结束记录:
zip 文件末尾的“中央目录结束记录”标识了中央目录的结束,包含中央目录的偏移量和大小等信息。
一个简单的 zip 文件可以包含多个文件的本地文件头、压缩数据、中央目录和结束记录。解压工具通过读取中央目录找到各个文件的偏移量和大小,然后根据这些信息读取和解压文件数据。
以下是一个 zip 文件结构的简化示例:
[本地文件头1] [文件数据1] [本地文件头2] [文件数据2] ... [中央目录] [中央目录结束记录]
zip 文件格式因其广泛的支持和高效的压缩性能,广泛应用于文件归档和传输。deflate 算法是其最常用的压缩算法,提供了良好的平衡点。
常用算法
压缩算法:
- deflate:这是 zip 文件中最常用的压缩算法,由 phil katz 发明。它结合了 lz77 算法和 huffman 编码,提供了良好的压缩比和解压速度。
- store:不进行任何压缩,仅用于存储数据。适用于已经被压缩过的数据,如 jpeg 图像或 mp3 音频文件。
- 其他压缩方法:zip 规范还支持其他压缩方法,如 bzip2、lzma 和 ppmd,但这些方法在实际使用中较为少见。
加密算法:
- 传统 zip 加密:早期的 zip 文件使用一种相对简单的对称加密方法,但这种方法的安全性较弱。
- aes 加密:一些现代的 zip 工具支持使用高级加密标准 (aes) 进行加密,提供了更强的安全性。
校验算法:
- crc-32:用于每个文件的数据完整性校验。每个文件都有一个 crc-32 校验和,用于检测数据传输或存储过程中的错误。
zip格式结构图总览

zip文件结构详解
zip格式压缩包主要由三大部分组成:数据区、中央目录记录区(也有叫核心目录记录)、中央目录记录尾部区。
数据区
数据区是由一系列本地文件记录组成,本地文件记录主要是记录了压缩前后文件的元数据以及存放压缩后的文件,组成部分也分为三大部分:本地文件头、文件数据、文件描述
本地文件头

local file header signature 4 bytes (0x04034b50) 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 (variable size) extra field (variable size)
本地文件头主要是记录了压缩文件的元数据:
- loca file header signature:0~3,4个字节,用来存放本地文件头标识,一般为固定值
0x04034b50,用于解压时候,读取判断文件头的开始; - version needed to extract:4~5,2个字节,记录解压缩文件所需的最低支持的zip规范版本,apk压缩版本默认是20, 即deflate压缩方式。该字段值=解压所需的最低zip规范版本*10,比如,最低支持的zip规范版本是2.0,那么该字段的值就是20。每个版本定义如下。
当前最低功能版本定义如下:(压缩包记录的解压版本都是需要版本*10,比如:2.0 * 10 = 20) 1.0 – 默认值 1.1 – 文件是卷标 2.0 – 文件是一个文件夹(目录) 2.0 – 使用 deflate 压缩来压缩文件 2.0 – 使用传统的 pkware 加密对文件进行加密 2.1 – 使用 deflate64™ 压缩文件 2.5 – 使用 pkware dcl implode 压缩文件 2.7 – 文件是补丁数据集 4.5 – 文件使用 zip64 格式扩展 4.6 – 使用 bzip2 压缩文件压缩 5.0 – 文件使用 des 加密 5.0 – 文件使用 3des 加密 5.0 – 使用原始 rc2 加密对文件进行加密 5.0 – 使用 rc4 加密对文件进行加密 5.1 – 文件使用 aes 加密进行加密 5.1 – 使用更正的 rc2 加密对文件进行加密 5.2 – 使用更正的 rc2-64 加密对文件进行加密 6.1 – 使用非 oaep 密钥包装对文件进行加密 6.2 – 中央目录加密
- genaral purpose bit flag:6~7,2个字节,记录通用标志位,第0位为
1时(即二进制:00000000 00000001),表示文件被加密,解压时候需要解密;第3位为1时候(即二进制:00000000 00000100),表示有数据描述部分,本地文件头中的 crc-32、压缩大小和未压缩大小字段都被设置为0(虽然zip规范是这么定义,但是发现有些压缩包即使声明有数据描述部分,但是本地文件头的crc-32、压缩大小和未压缩大小依然还是设置为真实值) , 正确的值被放在紧跟在压缩数据之后的数据描述部分,apk的通用标志位默认传0即可,也有传2048、2056,目前第15位是pkware保留位。 - compression method:8~9,2个字节,记录压缩包所用到的压缩方式,apk默认deflate压缩,传8即可, 要是传0 ,则是不压缩,各种压缩方式对应数值如下:
0 – the file is stored (no compression) 1 – the file is shrunk 2 – the file is reduced with compression factor 1 3 – the file is reduced with compression factor 2 4 – the file is reduced with compression factor 3 5 – the file is reduced with compression factor 4 6 – the file is imploded 7 – reserved for tokenizing compression algorithm 8 – the file is deflated 9 – enhanced deflating using deflate64™ 10 – pkware data compression library imploding 11 – reserved by pkware 12 – file is compressed using bzip2 algorithm
- last mod file time:10~11,2个字节,记录文件最后修改时间,是ms-dos格式编码的时间

- last mod date time:12~13,2个字节,记录文件最后修改日期,是ms-dos格式编码的日期
- crc-32:14~17,4个字节,记录文件未压缩时的crc-32校验码
- compressed size:18~21,4个字节,记录文件压缩后的大小
- uncompressed size:22~25,4个字节,记录文件未压缩的大小
- file name length:26~27,2个字节,记录文件名的长度(假设文件名长度为n)
- extra field length:28~29,2个字节,记录扩展区的长度(假设扩展区长度为m)
- file name:30~30+n,n个字节,记录文件名
- extral field:30+n~30+n+m,m个字节,记录扩展数据
文件数据
文件数据紧跟在本地文件头之后,一般是压缩后的文件数据或压缩方式选择不压缩时候,用来存储未压缩文件数据。
文件描述
文件描述符仅在通用位标志的第 3 位被设置为1时才存在。 它是字节对齐的,紧跟在文件数据的最后一个字节之后。当且仅当无法在 .zip 文件中查找时才使用此描述符,例如:当输出 的.zip 文件是标准输出或不可查找设备时使用文件描述,换句话说,正常情况下都不需要使用。

( 数据描述符标识不一定有,因为一开始规范是没有的,后面才加上去的)
中央目录记录区(核心目录记录区 )
中心目录区的结构如下。
[file header 1] . . . [file header n] [digital signature]
central file header signature 4 bytes (0x02014b50) version made by 2 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 comment length 2 bytes disk number start 2 bytes internal file attributes 2 bytes external file attributes 4 bytes relative offset of local header 4 bytes file name (variable size) extra field (variable size) file comment (variable size)
中央目录记录区是有一系列中央目录记录所组成,一条中央目录记录对应数据区中的一个压缩文件记录,中央目录记录由以下部分构成:(中央目录区通常由多个文件头(file header)组成,每一个被压缩的文件都有一个对应的file header(注意,这里不是local file header),用于标识和定位该文件在zip文件中的位置。这个文件头和本地文件头类似,记录了被压缩文件的元数据信息,包括文件原始大小,压缩之后的大小,文件注释等。)

- central file header signature:0~3,4个字节,记录核心目录文件头标识,固定值:
0x02014b50,用于解压时候,查找判断是否是中央目录的开始位置 - version made by:4~5,2个字节,记录压缩所用的版本,同数据区本地文件头的解压所需版本,apk设置20
- 6~7:2个字节,记录解压所需的最小版本,同数据区本地文件头的解压所需版本,apk设置20
- 8~9:2个字节,通用位标记,同数据区本地文件头的通用位标记
- 压缩方法、文件最后修改时间、文件最后修改日期、crc-32校验码、压缩后大小、未压缩大小、文件名长度、扩展区长度,这几个字段的含义都等同于数据区本地文件头对应字段的含义
- file comment length:32~33,2个字节,记录文件注释的长度
- disk number start:34~35,2个字节,记录文件开始位置的磁盘编号,一般传0即可
- 36~41:内部文件属性、外部文件属性,一般也是传0即可
- 42~45:4个字节,记录数据区本地文件头相对于压缩包开始位置的偏移量
数据签名(digital signature):
header signature 4 bytes (0x05054b50) size of data 2 bytes signature data (variable size)
- header signature:数字签名起始标识,固定值为0x05054b50。
- size of data:数字签名数据大小。
- signature data :签名数据
中央目录记录尾部区
中央目录记录尾部主要作用是用来定位中央目录记录区的开始位置,同时记录压缩包的注释内容:

end of central dir signature 4 bytes (0x06054b50) number of this disk 2 bytes number of the disk with the start of the central directory 2 bytes total number of entries in the central directory on this disk 2 bytes total number of entries in the central directory 2 bytes size of the central directory 4 bytes offset of start of central directory with respect to the starting disk number 4 bytes .zip file comment length 2 bytes .zip file comment (variable size)
- end of central dir signature:0~3,4个字节,中央目录记录尾部开头标记,固定值:
0x06054b50,用于解压时,查找判断中央目录尾部的起始位置 - number of this disk:4~5,2个字节,记录中央目录记录尾部区所在磁盘编号
- number of the disk with the start of the central directory:6~7,2个字节,记录中央目录开始位置所在的磁盘编号
- total number of entries in the central directory on this disk:8~9,2个字节,该磁盘上所记录的核心目录数量
- total number of entries in the central directory:10~11,2个字节,zip压缩包中的文件总数
- size of the central directory:12~15,4个字节,整个中央目录的大小(以字节为单位)
- offset of start of central directory with respect to the starting disk number:16~19,4个字节,中央目录开始位置相对位移
- zip file comment length:20~21,2个字节,注释内容的长度(假设长度为n)
- zip file comment:22~22+n,n个字节,注释内容
中央目录结束标识是zip文件解压的入口。通过读取中央目录结束标识,解压缩软件可以快速地找到中央目录,并据此解析整个zip文件的结构和内容。通过里面的中央核心目录区的大小可以找到对应的中央目录模块,然后根据中央目录文件头中的本地文件头偏移(relative offset of local header)可以寻址到对应的文件,并进行解压。
每个压缩文件都必须且仅有一个中央目录结束标识。如果zip文件损坏或结构不正确,可能会导致中央目录结束标识丢失或损坏,从而使得解压缩软件无法正确读取和解析zip文件。
压缩包解压过程
方式1 通过解析中央目录区来解压
通过zip文件的结构我们发现,zip文件的中央目录区保存了所有的文件信息。所以,可以通过中央目录区拿到所有的文件信息并进行解压,步骤如下所示。

- 首先在 zip 文件末尾通过中央目录结束标识 (0x06054b50)找到中央目录结束标识数据块。
- 通过中央目录结束标识中的中央目录区开始位置偏移找到中央目录区数据块。
- 根据中央目录区的file header中的 local file header的偏移量找到对应的local file header。
- 根据 local file header找到对应的file data
- 解密 file data(如果需要);
- 解压 file data;
方式2 通过读取本地文件头来解压
根据 zip 文件格式标准可知,除了 中央目录区, 本地文件头中也包含了每个文件的相关信息。因此,可以基于本地文件头去解压文件数据,其解压流程就可以变为:
- 从头开始,通过本地文件头标识搜索对应的 local file header
- 读取 local file header并找到file data;
- 解密 file data(如果需要);
- 解压 file data;
两种解压方式对比
通过两种解压方式可以明显看出,两种解压方式适用的场景不同。
方式1适用场景:
- 适用于在解压文件已经存在于磁盘上,并且需要解压压缩包中所有的文件。
方式2适用场景:
- 当文件不在磁盘上,比如从网络接收的数据,想边接收边解压;
- 需要顺序解压zip文件前面的一小部分文件,可以使用这种方式,因为方式1读中央目录区会带来额外的耗时;
- zip文件中的中央目录区遭到损坏;
golang解压zip包
官方archive/zip
golang zip包的解压有官方的zip包(archive/zip),但是官方给的zip解压包代码只有解压不带密码的zip包。
下面给出解压操作的封装:
func unzip(src, dst string) error {
// zip.newreader() 适合从stream中读取字节序列
zf, err := zip.openreader(src)
if err != nil {
return err
}
defer zf.close()
for _, file := range zf.file {
// fmt.println(file.name)
path := filepath.join(dst, file.name)
// 如果是目录则创建目录
if file.fileinfo().isdir() {
if err = os.mkdirall(path, 0o644); err != nil {
return err
}
continue
}
f, err := os.create(path)
if err != nil {
return err
}
reader, err := file.open()
if err != nil {
return err
}
_, err = io.copy(f, reader)
if err != nil {
return err
}
_ = f.close()
_ = reader.close()
}
return nil
}
func main() {
log.println(unzip("go.zip", "./static"))
}

通常情况下,目录权限设为 0755,文件权限设为 0644 更为合适。
在 unix 和 unix-like 操作系统(如 linux)中,文件权限使用三位八进制数来表示,每一位分别表示所有者(owner)、组(group)和其他人(others)的权限。每一位的权限可以是 1(执行权限),2(写入权限),4(读取权限)的组合。它们的组合值表示特定的权限设置。
权限位的含义:
- 1:执行权限 (execute)
- 2:写入权限 (write)
- 4:读取权限 (read)
这些权限可以相加来组合权限。例如:
- 7 (4+2+1):读取、写入和执行权限 (read + write + execute)
- 6 (4+2):读取和写入权限 (read + write)
- 5 (4+1):读取和执行权限 (read + execute)
- 4:读取权限 (read)
- 3 (2+1):写入和执行权限 (write + execute)
- 2:写入权限 (write)
- 1:执行权限 (execute)
- 0:无权限 (no permissions)
权限设置示例:
权限设置通常以三位八进制数表示,例如 0755,每一位代表不同用户类别的权限:
- 第一位(owner 权限):
7,表示所有者有读取、写入和执行权限。 - 第二位(group 权限):
5,表示组用户有读取和执行权限。 - 第三位(others 权限):
5,表示其他用户有读取和执行权限。
rwxr-xr-x
r代表读取权限w代表写入权限x代表执行权限-代表没有该权限
示例解释:
0755:
- 所有者(owner)权限:
7(rwx) 读取、写入和执行 - 组(group)权限:
5(r-x) 读取和执行 - 其他人(others)权限:
5(r-x) 读取和执行
0644:
- 所有者(owner)权限:
6(rw-) 读取和写入 - 组(group)权限:
4(r–) 读取 - 其他人(others)权限:
4(r–) 读取
第三方包github.com/yeka/zip
使用go get github.com/yeka/zip安装后,代码都不需要改变,只是导入的zip包替换为第三方即可:


https://github.com/yeka/zip,关于这个库里面的整个代码是在官方的zip库的基础上做了一些修改,并且这个库里面的代码抛弃了注册的功能,直接把解密代码写在了open文件里,废弃了一个很好用的功能。


自己实现解压加密的zip包
golang官方的zip代码库,https://golang.google.cn/pkg/archive/zip/#pkg-examples有两个注册接口,一个是压缩和一个是解压的:


官方注册压缩器方法示例:
package main
import (
"archive/zip"
"bytes"
"compress/flate"
"io"
)
func main() {
// override the default deflate compressor with a higher compression level.
// create a buffer to write our archive to.
buf := new(bytes.buffer)
// create a new zip archive.
w := zip.newwriter(buf)
// register a custom deflate compressor.
w.registercompressor(zip.deflate, func(out io.writer) (io.writecloser, error) {
return flate.newwriter(out, flate.bestcompression)
})
// proceed to add files to w.
}
类比这个案例可以写出:
func main() {
zf, _ := zip.openreader("")
zf.registerdecompressor(zip.deflate, func(r io.reader) io.readcloser {
// todo:解密算法实现
return flate.newreader(r)
})
}
接下来实现解密算法(官方链接:https://support.pkware.com/pkzip/appnote):

zip标准文件:https://pkwaredownloads.blob.core.windows.net/pem/appnote.txt,温馨提示:不要机器翻译成中文。

zip 文件加密算法通常使用一种简单的流加密方法,称为 zipcrypto。解密过程包括初始化三个 32 位整数 key0, key1, 和 key2,并根据密码和加密的字节数据更新这些值。加密和解密使用同样的逻辑,只是加密是将明文转换为密文,而解密是将密文转换为明文。
type zipcrypto struct {
password []byte
keys [3]uint32
}
func newzipcrypto(passphrase []byte) *zipcrypto {
z := &zipcrypto{}
z.password = passphrase
z.init()
return z
}
func (z *zipcrypto) init() {
z.keys[0] = 0x12345678
z.keys[1] = 0x23456789
z.keys[2] = 0x34567890
for i := 0; i < len(z.password); i++ {
z.updatekeys(z.password[i])
}
}
func (z *zipcrypto) updatekeys(bytevalue byte) {
z.keys[0] = crc32update(z.keys[0], bytevalue)
z.keys[1] += z.keys[0] & 0xff
z.keys[1] = z.keys[1]*134775813 + 1
z.keys[2] = crc32update(z.keys[2], (byte)(z.keys[1]>>24))
}
func (z *zipcrypto) magicbyte() byte {
var t uint32 = z.keys[2] | 2
return byte((t * (t ^ 1)) >> 8)
}
func (z *zipcrypto) encrypt(data []byte) []byte {
length := len(data)
chiper := make([]byte, length)
for i := 0; i < length; i++ {
v := data[i]
chiper[i] = v ^ z.magicbyte()
z.updatekeys(v)
}
return chiper
}
func (z *zipcrypto) decrypt(chiper []byte) []byte {
length := len(chiper)
plain := make([]byte, length)
for i, c := range chiper {
v := c ^ z.magicbyte()
z.updatekeys(v)
plain[i] = v
}
return plain
}
func crc32update(pcrc32 uint32, bval byte) uint32 {
return crc32.ieeetable[(pcrc32^uint32(bval))&0xff] ^ (pcrc32 >> 8)
}
实现加密解密算法后写一个小例子来测试下:
func main() {
password := "generalzy"
zc := newzipcrypto(password)
// 示例数据
data := []byte("hello, zipcrypto!")
fmt.printf("original: %s\n", data)
// 加密数据
encrypted := zc.encrypt(data)
fmt.printf("encrypted: %x\n", encrypted)
// 初始化解密器
zcdecrypt := newzipcrypto(password)
// 解密数据
decrypted := zcdecrypt.decrypt(encrypted)
fmt.printf("decrypted: %s\n", decrypted)
}

要利用 zip 包提供的注册方法来注册解密函数,可以使用 registerdecompressor 方法。首先,需要扩展之前的 zipcrypto 实现,使其能够解密压缩数据流。然后,可以将这个解密流注册到 zip 包中。
最后给出整体代码:
package main
import (
"archive/zip"
"bytes"
"compress/flate"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"os"
"path/filepath"
)
type zipcrypto struct {
password []byte
keys [3]uint32
}
func newzipcrypto(passphrase []byte) *zipcrypto {
z := &zipcrypto{}
z.password = passphrase
z.init()
return z
}
func (z *zipcrypto) init() {
z.keys[0] = 0x12345678
z.keys[1] = 0x23456789
z.keys[2] = 0x34567890
for i := 0; i < len(z.password); i++ {
z.updatekeys(z.password[i])
}
}
func (z *zipcrypto) updatekeys(bytevalue byte) {
z.keys[0] = crc32update(z.keys[0], bytevalue)
z.keys[1] += z.keys[0] & 0xff
z.keys[1] = z.keys[1]*134775813 + 1
z.keys[2] = crc32update(z.keys[2], (byte)(z.keys[1]>>24))
}
func (z *zipcrypto) magicbyte() byte {
var t uint32 = z.keys[2] | 2
return byte((t * (t ^ 1)) >> 8)
}
func (z *zipcrypto) encrypt(data []byte) []byte {
length := len(data)
chiper := make([]byte, length)
for i := 0; i < length; i++ {
v := data[i]
chiper[i] = v ^ z.magicbyte()
z.updatekeys(v)
}
return chiper
}
func (z *zipcrypto) decrypt(chiper []byte) []byte {
length := len(chiper)
plain := make([]byte, length)
for i, c := range chiper {
v := c ^ z.magicbyte()
z.updatekeys(v)
plain[i] = v
}
return plain
}
func crc32update(pcrc32 uint32, bval byte) uint32 {
return crc32.ieeetable[(pcrc32^uint32(bval))&0xff] ^ (pcrc32 >> 8)
}
func zipcryptodecryptor(r *io.sectionreader, password []byte) (*io.sectionreader, error) {
z := newzipcrypto(password)
b := make([]byte, r.size())
r.read(b)
m := z.decrypt(b)
return io.newsectionreader(bytes.newreader(m), 12, int64(len(m))), nil
}
type unzip struct {
offset int64
fp *os.file
name string
}
func (uz *unzip) init() (err error) {
uz.fp, err = os.open(uz.name)
return err
}
func (uz *unzip) close() {
if uz.fp != nil {
uz.fp.close()
}
}
func (uz *unzip) size() int64 {
if uz.fp == nil {
if err := uz.init(); err != nil {
return -1
}
}
fi, err := uz.fp.stat()
if err != nil {
return -1
}
return fi.size() - uz.offset
}
func (uz *unzip) readat(p []byte, off int64) (int, error) {
if uz.fp == nil {
if err := uz.init(); err != nil {
return 0, err
}
}
return uz.fp.readat(p, off+uz.offset)
}
// decompresszip 解压zip包
func decompresszip(zipfile, dest, passwd string, offset int64) error {
uz := &unzip{offset: offset, name: zipfile}
defer uz.close()
zr, err := zip.newreader(uz, uz.size())
if err != nil {
return err
}
if passwd != "" {
// register a custom deflate compressor.
zr.registerdecompressor(zip.deflate, func(r io.reader) io.readcloser {
rs := r.(*io.sectionreader)
r, _ = zipcryptodecryptor(rs, []byte(passwd))
return flate.newreader(r)
})
zr.registerdecompressor(zip.store, func(r io.reader) io.readcloser {
rs := r.(*io.sectionreader)
r, _ = zipcryptodecryptor(rs, []byte(passwd))
return ioutil.nopcloser(r)
})
}
for _, f := range zr.file {
fpath := filepath.join(dest, f.name)
if f.fileinfo().isdir() {
os.mkdirall(fpath, os.modeperm)
continue
}
if err = os.mkdirall(filepath.dir(fpath), os.modeperm); err != nil {
return err
}
infile, err := f.open()
if err != nil {
return err
}
outfile, err := os.openfile(fpath, os.o_wronly|os.o_create|os.o_trunc, f.mode())
if err != nil {
infile.close()
return err
}
_, err = io.copy(outfile, infile)
infile.close()
outfile.close()
if err != nil {
return err
}
}
return nil
}
func main() {
err := decompresszip("go.zip", "./tmp", "123456", 0)
if err != nil {
fmt.println(err)
}
return
}
以上就是golang解压带密码的zip包的方法详解的详细内容,更多关于golang解压zip包的资料请关注代码网其它相关文章!
发表评论