为什么是ZIP?
在日常开发或者折腾的时候,我们常常习惯于在GUI界面中右键点击“解压到当前文件夹”。直到linux环境下,我在解压一个8GB分卷压缩包的时候,(.z01, .z02, .zip),报错Is not archive或Unexpected end of archive的时候,我才意识到这个每天都在和我们打交道的ZIP,底层藏着一套非常精妙而又充满历史妥协的设计。
本文记录在一次Linux(my distro is arch)下排查并修复损坏Zip64分卷的实操,并从尝试从底层进行分析:为什么在ZIP的目录在文件末尾?为什么cat拼接会导致索引错位?zip -FF又是如何修复我们的压缩包索引问题的?
ZIP的起源
在现代操作系统的文件系统(例如Linux中经典的ext4/Btrfs,Windows中的NTFS)管理着数以万计的离散小文件。在传输场景下,几万个小文件会带来极大的元数据寻址开销和网络握手延迟。那么我们有没有办法把离散的小文件归档成为一个文件呢?所以,归档格式的诞生,本质上是为了将树状的文件系统层次序列化为单一的连续字节流。
与其他归档格式相比(例如tar仅仅只是负责打包、gzip负责的是流式压缩。
zip文件的结构

如上图所示:ZIP File由多个File Header组成,每个header都由一个签名和头部数据标识组成,我们来看看这些不同的header所代表的信息:
- Local File Header: 包含单个文件的元数据,后面跟着的就是文件数据。每个文件都可以进行独立进行压缩和加密。
- Central directory file header: 包含单个文件的元数据以及该文件本地文件头的偏移量。这些文件头的列表构成中央目录,可用于快速枚举和归档文件中的所有文件。
- End of central directory record: 包含有关在哪里找到第一个中央目录文件头的信息
我们知道,在计算机中,所有的程序都是有一个entry point(接入点的),对于Zip File来说,这个接入点就是End of central directory record。他始终位于文件的末尾。上图所示
如果我们要追加一个新的压缩文件c.txt呢?那么就会和下图一样:

发现了吗?中央目录会一直位于底部的,我们如果添加了新的文件c.txt,会添加一个完整的Central directory file header,这个新的header就作为新的Central directory file header存在了,而旧的并不会被覆盖,他仍然存在,但是他不会被引用,这样,我们没有重写文件,但是我们修改了归档文件的相关信息。
这个行为被发明出来是为了在软盘通信时代中解决写入速度慢和zip跨磁盘读取和写入的问题的。在早期软盘和流式磁带的时代,程序在压缩文件时是边读边压边写的。在把最后一个文件压缩写完之前,程序根本无法预知每个文件的最终体积以及确切的偏移量。
因此,ZIP采取了“先写数据,最后在尾部写一个EOCD”的设计。解压软件在打开ZIP的时候,通过entry point,即EOCD签名的方式,拿到中央目录后再去按图索骥读取具体的数据块。
当然了,除了上面说的添加文件之外,我们还可以通过追加文件来实现修改和删除压缩包中的文件。当然了被删除的文件和修改后的文件的先前版本会保留在磁盘上,但他们无法被兼容ZIP的读取器读取

NOTE一些有趣的事实和小技巧:由于ZIP文件是从末尾读取的,因此可以在ZIP文件的开头放置任何内容。例如,我们可以创建一个既是有效PNG文件又是有效ZIP文件(polyglot),从而将一个zip文件隐藏在看起来像图像且功能正常的文件中。 如果在自己编写zip的解压文件时(
这大概率是下一篇博客的内容 ),还可以将ZIP文件附加到ZIP解压缩程序的二进制文件中,使其在运行时自动解压缩。这个技术被称为自解压归档(self-extracting archive),是一种常见的双击文件自动锁解压缩的技术 ZIP格式允许在文件中的多个位置添加”注视”。这些注释可以存储任意数据,从而使ZIP文件与其他格式兼容,而不仅仅是ZIP格式
zip的多P/多磁盘压缩包
ZIP格式允许将一个archive分割成多个文件,这种格式最初是为了将大型压缩文件存储在多张软盘上而实现的。现如今,在我们使用各种上传文件或者利用电子邮件传输的时候,仍然可以通过这种方式发送大型文件,来绕过文件大小的限制(例如百度网盘分p等)
在规范中,提到了“磁盘编号”等字段,在处理多部分归档文件中,这个相当于是“文件编号”
为什么我们可以使用cat拼接分卷呢?
分卷的本质在于将连续的ZIP字节流按指定大小物理切块
cat archive.z01 archive.z02 archive.zip > full.zip在unit哲学中,“一切皆文件,文件皆字节流”。cat做的事情就是把多个文件中的二进制字节流原原本本按照顺序拼接在一起,输出为一个平坦的数据块。理论上,这可以直接还原被切片前的原始数据流。
不过,在实际规范中存在一个问题,我们有的时候可能图方便,拼接的顺序并不是按分卷的顺序进行拼接的,这个时候会导致一个问题:如果你使用7z x full.zip的时候,会看到以下报错
Characteristics = Local Zip64Embedded Stub Size = 4ERRORS: Unexpected end of archive / Open Errors: 1在这里,我们可以看到两个内容,第一个:zip64是什么?
在传统的ZIP规范中,文件大小与偏移量均使用32位无符号整数表示,最大寻址空间只有2^32-1≈4GB 当我们的归档体积达到8GB的时候,它会触发zip64的扩展协议。zip64在普通的EOCD前面会插入一个Zip64 End of Central Directory Record,使用64位进行寻址。
由于原始的分卷第一卷包含了4字节的跨卷签名,或者某些打包软件在切片的时候插入了卷头,如果我们使用cat直接进行强制拼合,会导致中央目录内记录的每个文件的相对便宜地址(Offsets)与实际字节流产生了微小的唯一偏差。
当7-Zip读取末尾的Zip64 EODC时,发现按照索引去寻址对应的数据块时碰到了非法的字节,或者索引指向的物理终点超出了文件边界,于是抛出了Unexpected end of archive
如何使用zip修复呢?
没错,ZIP自己可以修复这个问题。面对索引错位的拼接包,我们可以直接执行这条指令:
zip -FF full.zip --out fixed.zipzip命令自带的-F(fix)和-FF(fix fix强力修复),可以帮助我们修复这个损坏的内容:
- 放弃损坏的中央目录:它不会再使用文件自带的可能损坏的或者偏移存在问题的EOCD和Central Directory。
- 从头遍历Local File Header:从偏移量0x00000000开始,以字节滑动窗口扫描所有符合
0x04034b50(Local File Header标识符)的特征码 - 重新测算物理偏移量:为每一个成功扫描到完整数据块重新计算它在新文件中确切的Offset、文件大小与校验信息
- 生成全新的中央目录树:在文件末尾重新生成一个完全合法、干净、符合当前物理结构的全新Central Directory与EOCD(包括Zip64结构)
TIP除了
-FF修复,日常维护中zip还有很多实用的高级参数
zip -T archive 可以测试完整性而不解压
zip -s 0 split_archive.zip —out single_archive.zip 可以将多卷合并为单卷
zip -u archhive.zip src/* 更新/同步压缩包内容(rsync like)
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






