Zip 文件格式现在已有 32 年的历史。你会认为已经 32 岁了,格式会被很好地记录下来。不幸的是,事实并非如此。我有一种感觉,这就像许多文件格式一样。它们不是被设计出来的,而是开发人员在设计过程中自行制作的。如果它变得流行,其他人想要阅读和/或编写它们。他们要么尝试对格式进行逆向工程,要么要求规范。即使开发人员编写规范,他们也经常忘记他们原始程序所做的所有假设。这些没有写下来,因此规范是不完整的。 Zip就是这样一种格式。 Zip 声称其格式记录在名为 APPNOTE.TXT 的文件中,可在此处找到。简短的版本是,一个 zip 文件由记录组成,每个记录以一些 4 字节标记开始,通常采用以下形式 其中 0x50、0x4B 是字母 PK,代表“Phil Katz”,即制作 zip 格式的人。他们俩 ??是标识记录类型的字节。示例 0x50 0x4b 0x03 0x04 // 本地文件记录 0x50 0x4b 0x01 0x02 // 中央目录文件 record0x50 0x4b 0x06 0x06 // 中央目录记录的结尾 记录不遵循任何标准模式。要阅读甚至跳过记录,您必须知道其格式。我的意思是还有其他几种格式遵循某些约定,例如每个记录 idis 后跟记录的长度。所以,如果你看到一个 id,但你不理解它,你只需读取长度,跳过那么多字节 (*),你就会在下一个 id。这种类型的示例包括大多数视频容器格式、jpg、tiff、photoshop 文件、wav 文件等。 (*) 某些格式需要将长度四舍五入到最接近的 4 或 16 的倍数。
Zip 不会这样做。如果您看到一个 id 并且您不知道该类型记录的内容是如何构建的,则无法知道要跳过多少字节。 4.1.9 ZIP 文件可以流式传输、分割成段(在固定或可移动媒体上)或“自解压”。自解压 ZIP 文件必须在 ZIP 文件中包含目标平台的解压代码。 4.3.1 ZIP 文件必须包含“中央目录记录的结尾”。仅包含“中央目录记录结尾”的 ZIP 文件被视为空 ZIP 文件。文件可以在 ZIP 文件中添加或替换,或删除。 ZIP 文件必须只有一个“中央目录记录的结尾”。本规范中定义的其他记录可以根据需要用于支持单个 ZIP 文件的存储要求。 4.3.2 放入 ZIP 文件的每个文件必须以该文件的“本地文件头”记录开头。每个“本地文件头”必须在 ZIP 文件的中央目录部分内附有相应的“中央目录头”记录。 4.3.3 文件可以以任意顺序存储在 ZIP 文件中。一个 ZIP 文件可以跨越多个卷,也可以分成用户定义的段大小。所有值必须以小端字节顺序存储,除非本文档中对特定数据元素另有规定。 [本地文件头1] [加密头1] [文件数据1] [数据描述符1] 。 . . [本地文件头n] [加密头n] [文件数据n] [数据描述符n] [归档解密头] [归档额外数据记录] [中央目录头1] 。 . . [中心目录头 n] [中心目录记录的 zip64 结尾] [中心目录定位器的 zip64 结尾] [中心目录记录的结尾] 本地文件头签名 4 字节 (0x04034b50) 版本需要提取 2 字节 通用位标志 2 字节压缩方法 2 字节上次修改文件时间 2 字节上次修改文件日期 2 字节 crc-32 4 字节压缩大小 4 字节未压缩大小 4 字节文件名长度 2 字节额外字段长度 2 字节文件名(可变大小) 额外字段(可变大小) )
紧跟在文件的本地头之后应该放置文件的压缩或存储数据。如果文件被加密,文件的加密头应该放在本地头之后,文件数据之前。 [本地文件头][加密头][文件数据][数据描述符]系列对.ZIP 存档中的每个文件重复。零字节文件、目录和其他不包含内容的文件类型不得包含文件数据。中央文件头签名 4 字节 (0x02014b50) 版本 2 字节版本需要提取 2 字节通用位标志 2 字节压缩方法 2 字节上次修改文件时间 2 字节上次修改文件日期 2 字节 crc-32 4 字节压缩大小 4字节未压缩大小 4 字节文件名长度 2 字节额外字段长度 2 字节文件注释长度 2 字节磁盘编号开始 2 字节内部文件属性 2 字节外部文件属性 4 字节本地头的相对偏移 4 字节文件名(可变大小)额外字段(可变大小) 文件注释 (可变大小) 中央目录的结尾签名 4 字节 (0x06054b50) 此磁盘的编号 2 字节的磁盘编号以及中央目录的开头 2 字节 此磁盘上中央目录中的总条目数2 字节中央目录中的条目总数 2 字节中央目录大小 4 字节中央目录起始位置相对于起始磁盘号的偏移量 4 字节 .ZIP 文件注释 len gth 2 字节 .ZIP 文件注释(可变大小) 还有其他细节涉及加密、更大的文件、可选数据,但就本文而言,这就是我们所需要的。我们还需要一条信息,即如何制作自解压存档。为此,我们可以回顾一下 1989 年与 pkzip 一起提供的 ZIP2EXE.exe,看看它做了什么,但更容易查看 Info-Zip 来了解发生了什么。该过程基本上在 UnZipSFX 手册页中描述。首先为您的目标平台(DOS、Windows、OS/2 等)获取合适的 UnZip 二进制发行版,如上所述;我们将在下面的例子中假设 DOS。然后从发行版中提取 UnZipSFX 存根并将其作为本机 Unix 存根添加:
> unzip unz552x3.exe unzipsfx.exe // 提取 DOS SFX 存根> cat unzipsfx.exe yourzip.zip > yourDOSzip.exe // 创建 SFX 存档> zip -A yourDOSzip.exe // 修复内部偏移> 就是这样。您仍然可以测试、更新和删除存档中的条目;这是一个功能齐全的zipfile。从前面扫描,当您看到记录的 id 时,请执行相应的操作。从后面扫描,找到end-of-central-directory-record然后用它来通读中心目录,只看中心目录引用的东西。从背面扫描是原始 pkunzip 的工作方式。一方面,这意味着如果您要求某些文件子集,它可以直接跳转到您需要的数据,而不必扫描整个 zip 文件。如果 zip 文件跨越多个软盘,这一点尤其重要。但是,4.1.9 说您可以流式传输 zip 文件。这怎么可能?如果有一些本地文件记录没有被中央目录引用怎么办?那有效吗?这是未定义的。好的?这表明中央目录可能不会引用 zip 文件中的所有文件,否则这个关于文件被添加、替换或删除的声明在规范中没有意义。
如果我有包含文件 A、B、C 的 file1.zip 并且我生成只包含文件 A、B 的 file2.zip。那些只是 2 个独立的 zip 文件。放入可以添加的规范是零意义的,替换和删除文件,除非知道一些如何影响 zip 文件格式的知识。 [本地文件A] [本地文件B] [本地文件C] [中央目录文件A] [中央目录文件C] [中央目录结尾] 然后明确删除B,因为中央目录没有引用它。另一方面,如果没有 [本地文件 B],那么您只有一个独立的 zip 文件,独立于其中包含 B 的其他一些 zip 文件。规范甚至不需要提及这种情况。 [本地文件A(旧)] [本地文件B] [本地文件C] [本地文件A(新)] [中央目录文件A(新)] [中央目录文件B] [中央目录文件C] [结束中央目录] 然后根据中央目录将 A(旧)替换为 A(新)。另一方面,如果没有 [本地文件 A(旧)],则您只有一个独立的 zip 文件。您可能认为这是无稽之谈,但您必须记住,pkzip 来自软盘时代。读取整个 zip 文件的内容并写出一个全新的 zip 文件可能是一个非常缓慢的过程。在这两种情况下,仅通过更新中央目录来删除文件的能力,或者通过读取现有中央目录、附加新数据、然后写入新中央目录来添加文件的能力都是理想的特性。如果您有一个跨越多个软盘的 zip 文件,则尤其如此;这在 1989 年很常见。您希望能够更新 zip 文件中的 README.TXT,而无需重新编写多张软盘。该格式最初打算从头到尾写入,因此在知道并写入 ZIP 中包含的所有文件后,可以最后写出中央目录和中央目录记录的结尾。如果添加文件,则无需重写整个文件即可应用更改。这就是最初的 PKZIP 应用程序设计用于编写 .ZIP 文件的方式。读取时,会先读取中心目录的ZIP文件末尾,定位到中心目录,再查找需要访问的文件
规范是否未定义中央目录未引用的本地文件。仅通过提及暗示:如果中央目录不引用所有本地文件是有效的,则通过从前端扫描来读取zip文件可能会失败。如果不特别小心,您会得到不应该存在的文件或试图覆盖现有文件的错误。但是,这与 4.1.9 所说的 zip 文件可能被流化相矛盾。如果可以流式传输 zip 文件,那么上面的两个示例都将失败,因为在第一种情况下,我们会在第二种情况下看到文件 Band,我们会在看到中央目录未引用它们之前看到文件 A(旧)。如果您必须等待中央目录才能正确使用任何条目,那么在功能上您不能流式传输 zip 文件。看到上面关于如何创建自解压zip文件的说明,我们只是在文件前面添加一些可执行代码,然后修复中央目录中的偏移量。 switch (id) { case 0x06054b50: read_end_of_central_directory();休息;案例 0x04034b50: read_local_file_record();休息;案例 0x02014b50: read_center_file_record();休息; ...} 鉴于上面的代码,很可能这些值 0x06054b50、0x04034b50、0x02014b50 会以二进制形式出现在文件前面的 zip 文件的自解压部分。如果您通过从前面扫描您的扫描仪来读取 zip 文件,我会看到这些 ID 并将它们错误地解释为 zip 记录。事实上,你可以想象一个带有 zip 文件的自解压器是这样的
// 包含// LICENSE.txt// README.txt// player.execonst unsigned char[] runtimeAndLicenseData = { 0x50, 0x4b, 0x03, 0x04, ??, ??, ...}; int main() { extractZipFromFile(getPathToSelf()); extractZipFromMemory(runtimeAndLicenseData, sizeof(runtimeAndLicenseData));} 现在自解压器中有一个 zip 文件。任何从前端读取的读者都会看到这个内部 zip 文件并失败。这是一个有效的 zip 文件吗?这是规范未定义的。我测试了这个。原来DOS下的PKUnzip.exe,Windows资源管理器,MacOS Finder,Info-Zip(MacOS和Linux自带的解压包),都是从后面看清楚,自解压后看到的文件。 7z,Keka,见自解压器内嵌的zipin。那是失败还是错误的 zip 文件? APPNOTE.TXT 就不说了。我认为它在这里应该是明确的,我认为这是那些未说明的假设之一。PKunzip 从背面扫描,所以这恰好可以工作,但它如何工作的事实从未被记录下来。自解压器中的数据可能碰巧类似于 zip 文件的问题被掩盖了。如果之前的问题还没有,类似的流式传输可能会失败。您可能认为这不是问题,但它们是 1990 年代存档中成百上千的自解压 zip 文件。前向扫描器可能无法读取这些。如果您查看上面的 4.3.16,您会看到 zip 文件的结尾是一个可变长度的注释。所以,如果你在做反向扫描,你基本上从文件的后面读取 0x50 0x4B 0x05 0x06 但是如果该字节序列在注释中呢?我敢肯定 Phil Katz 从未想过它。他只是假设人们会把相当于 README.txt 的文件放在那里。因此,它只有从 0x20 到 0x7F 的值,可能有 0x0D(回车)、0x0A(换行)、0x09(制表符)和 0x06(铃)。
不幸的是,id 中的所有这些值都是有效的 ASCII,甚至是 utf-8。我们已经讨论了 0x50 = P 和 0x4B = K。0x06 是 ASCII 中的“Bell”(发出噪音或闪烁屏幕)。 0x05 是“查询”。但是,这是什么意思?这是否意味着字节 0x50 0x4B 0x05 0x06 不能出现在注释或自提取代码中?这是否意味着您第一次看到从背面扫描时不会尝试找到第二个匹配项?如果您从正面扫描并且没有遇到前面提到的任何问题,那么前向扫描仪将成功读取此内容。另一方面,pkunzip 本身会失败。该偏移量是 0x504b0506,因此它似乎是结束中央目录标题。我认为创建 zip 时甚至没有 1.3gig zip 文件,并且实际上需要扩展来处理大于 4gig 的文件。但是,它确实显示了格式设计不佳的另一种方式。关于什么是好的设计肯定会有争论,但可以说,决定我们是否可以重新开始很容易。如果记录具有固定格式(例如 id 后跟大小)会更好,这样您就可以跳过您不理解的记录。如果文件末尾的最后一条记录只是一个偏移到中央目录结尾的记录会更好,如
0x504b0609 (id: 某些 id 未使用) 0x04000000 (记录数据的大小) 0x???????? (相对于 end-of-central-directory)检查前 8 个是 0x50 0x4b 0x06 0x09 0x04 0x00 0x00 0x00。如果没有,则失败。或者,相反,将注释放在自己的记录中并写在中央目录之前,并在中央目录记录的末尾放置一个偏移量。那么至少扫描评论的问题会消失。如果您想支持从前面阅读,似乎需要声明自提取部分不能出现任何记录。除非你专门写了一些验证器,否则很难强制执行。如果你只是根据你自己的应用程序是否可以读取 zip 文件来检查,那么就目前而言,Pkzip、pkunzip、info-zip(MacOS、Linux 中的 zip)、Windows Explorer 和 MacOS 都不关心自提取部分中的内容,因此它们对验证没有用。您必须明确声明您必须在规范中从背面扫描,或者编写一个拒绝不可向前扫描的 zip 的验证器,并在规范中说明原因。后向扫描器不关心记录之间的内容。它只关心它能找到中心目录,它只读取中心目录指向的内容。这意味着记录(或至少某些记录)之间可以有任何随机数据。如果我要猜测所有这些问题都是没有进入 APPNOTE.TXT 的实现细节。我认为 APPNOTE.TXT 真正想说的是“一个有效的 zip 文件是 pkzip 可以操作而 pkunzip 可以正确解压缩的文件。相反,它以各种实现可以制作其他实现无法读取的文件的方式定义事物。
当然,有了 32 年的 zip 文件,我们无法修复格式。 PKWare 可以做的是具体了解这些边缘情况。如果是我,我会将这些部分添加到 APPNOTE.TXT 4.3.1 ZIP 文件必须包含“中央目录记录的结尾”。仅包含“中央目录记录结尾”的 ZIP 文件被视为空 ZIP 文件。文件可以在 ZIP 文件中添加或替换,或删除。 ZIP 文件必须只有一个“中央目录记录的结尾”。本规范中定义的其他记录可以根据需要用于支持单个 ZIP 文件的存储要求。 “中央目录记录的结尾”必须在文件的末尾,并且字节序列 0x50 0x4B 0x05 0x06 不能出现在注释中。 “中央目录”是 zip 文件内容的权威。只有它引用的数据才能从文件中读取。这是因为 (1) 文件自解压部分的内容未定义,可能看起来包含 zip 记录,但实际上它们与 zip 文件无关,以及 (2) 添加、更新和删除的能力zip 文件中的文件源于这样一个事实,即只有中央目录知道哪些本地文件是有效的。那将是一种方式。我相信这将读取数以百万计的现有 zip 文件。另一方面,如果 PKWare 声称具有这些问题的此类文件不存在,那么这也会起作用。 4.3.1 ZIP 文件必须包含“中央目录记录的结尾”。仅包含“中央目录记录结尾”的 ZIP 文件被视为空 ZIP 文件。文件可以在 ZIP 文件中添加或替换,或删除。 ZIP 文件必须只有一个“中央目录记录的结尾”。本规范中定义的其他记录可以根据需要用于支持单个 ZIP 文件的存储要求。
“中央目录记录的结尾”必须在文件的末尾,并且字节序列 0x50 0x4B 0x05 0x06 不能出现在注释中。不能有没有出现在中央目录中的【本地文件记录】。这种保证是必需的,因此从前到后读取文件提供与从后到前读取相同的结果。任何不遵循此规则的文件都是无效的 zip 文件。自解压 zip 文件不得包含本文档中列出的任何记录 ID 序列,因为它们可能会被正向扫描 zip 阅读器错误解释。任何不遵循此规则的文件都是无效的 zip 文件。我希望他们会更新 APPNOTE.TXT,以便不同的 zip 阅读器和 zip 创建者可以就什么是有效的达成一致。不幸的是,我觉得 pkware 不想在这里说清楚。他们的 POV 似乎是 zip 是一种模棱两可的格式。如果您想通过从正面扫描来阅读,那么请不要尝试以这种方式阅读您无法阅读的文件。它们仍然是有效的 zip 文件,但您无法阅读它们的事实无关紧要。不支持那些只是你的选择。我想这是一个有效的 POV。很少有 zip 库可以处理 zip 的所有功能。不过,很高兴知道您是否故意不处理某些事情,或者您是否只是错误地读取了文件并且很幸运它有时会起作用。出现这一切的原因是我写了一个 javascript 解压缩库。这里有很多东西,但我有特殊需求,我发现其他库无法处理。特别是我需要一个库,让我尽可能快地从大 zip 中读取单个文件。这意味着向后扫描,找到所需文件的偏移量,然后解压缩该单个文件。希望其他人觉得它有用。