ZIP32文件格式详解

共 4388字,需浏览 9分钟

 ·

2022-01-03 09:28

ZIP32文件格式详解

为什么要去了解ZIP文件格式

最近有个需求,需要加载jar包中的jar包中的class,此时有两种方式:

  • 1、将jar解压缩,然后将解压缩后的路径添加到class path,这样就不存在嵌套jar的读取方式了,tomcat就是采用的这种方式;

  • 2、不解压缩jar,直接定位到jar包中的jar包中的class,然后将其数据抽离出来,将其解析为class注册到系统中去(可以利用ClassLoader的defineClass方法实现),spring-boot就是采用的这种方式;

最终我们决定使用第二种方案(解压可能会导致我们的环境污染,多出来许多文件),即不解压缩jar包,而这就需要我们深入了解jar包的格式,由于jar包就是ZIP格式的,所以要实现该功能必须得了解ZIP格式,这样才能从中抽取我们需要的数据;

ZIP文件设计

。ZIP文件是存储多个文件的存档。ZIP允许使用许多不同的方法压缩包含的文件,以及简单地存储文件而不压缩它。每个文件都单独存储,允许使用不同的方法压缩同一存档中的不同文件。由于 ZIP 存档中的文件是单独压缩的,因此可以提取它们或添加新文件,而无需对整个存档应用压缩或解压缩。

ZIP文件有一个目录,记录了ZIP文件中的各个文件的位置以及大小等信息,目录放置在 ZIP 文件的末尾。这允许ZIP读取器加载文件列表,而无需读取整个ZIP存档。ZIP 存档还可以包含与 ZIP 存档无关的额外数据。这允许将ZIP存档预置到ZIP存档中并将文件标记为可执行文件,从而将ZIP存档制作成自解压存档(解压缩其包含数据的应用程序)。将目录存储在末尾还可以通过将压缩文件附加到无害的文件(如 GIF 图像文件)来隐藏压缩文件。

个人理解:ZIP文件的这种特性也就意味着我们不仅仅可以在前边放置自解压程序,还可以防止一些渗透攻击程序,也就是说ZIP文件可能是不安全的,别人可能用一个ZIP文件来攻击我们;

ZIP文件格式

ZIP文件通常由以下五部分组成:

  • local file header(每个文件对应一个)

  • data(文件实际数据,可能压缩,也可能未压缩)

  • data descriptor(条件存在)

  • central directory file header(中央目录,每个文件一条记录)

  • end of central directory record(后续简称 EOCD 全局只有一个)

当我们将多个文件压缩为一个ZIP包时,多个文件在ZIP包中是分别存储的,每个文件都会生成一个entry,entry包括最前边的 local file header 、中间的data(实际的文件数据,可能被压缩了,也可能没压缩)以及末尾的data descriptor(这个是有条件的存在,后续会介绍);所有文件组成的entry存储完毕后生成 central directory file header ,每个文件都会对应一个 central directory file header ,所有的 central directory file header 结束后会添加一个 EOCD ,这个是一个ZIP文件只有一个;

最终我们的ZIP文件存储结构如下:





ZIP文件各个部分详细说明

local file header定义

offsetbytesdescription
04local file header签名,固定值:0x04034b50
42提取本文件需要的最低版本号
62标志位,如果第三个bit被设置了(0x08),表示写入的时候不知道数据大小和CRC-32,则该entry包含data descriptor部分
82压缩方法,0表示本entry没有压缩,只是归档到ZIP中了,0x08表示使用了DEFLATE算法压缩
102文件最后修改时间
122文件最后修改日期
144文件压缩前的CRC-32
184文件压缩后的大小,单位byte(如果是0xffffffff则表示是ZIP64文件,我们暂时不关心ZIP64,因为目前jar基本都是ZIP32的)
224文件压缩前的大小,单位byte(如果是0xffffffff则表示是ZIP64文件)
262文件名长度,单位byte,这表示我们的文件名长度不能超过65535 byte,并且这里的文件名是包含前边的目录的
282扩展字段长度,单位byte(这个我们暂时并不关心,所以无需去解析)
30n文件名
30 + nm扩展字段

从这个local file header定义可以看出local file header实际上是变长的,并不是定长的(因为文件名和扩展字段长度都是不固定的);

data descriptor

如果entry的local file header的标志位第三个bit被设置了(0x08),表示写入的时候不知道数据大小和CRC-32,则该entry包含本部分;

offsetbytesdescription
04签名,固定值:0x08074b50
44文件压缩前的CRC-32
84文件压缩后的大小,单位byte
124文件压缩前的大小,单位byte

central directory file header

每个entry都有一个central directory file header记录,central directory file header中的大部分信息都是冗余的local file header(或者data descriptor)中的信息,然后就是entry的定位信息,主要就是为了根据中央目录快速定位entry,而不用读取完整的整个文件去查找某个文件,这在早期磁盘较小(意味着一个ZIP文件可能存储于多个磁盘)、读取速度较慢的场景下极为有用,相当于索引;

offsetbytesdescription
04签名,固定值:0x02014b50
42制作于那个ZIP版本
62解析该目录需要的最低版本号,与对应entry的local file header中的值一致
82一般标志位,与对应entry的local file header中的值一致
102压缩方法,与对应entry的local file header中的值一致
122文件最后修改时间,与对应entry的local file header中的值一致
142文件最后修改日期,与对应entry的local file header中的值一致
164文件压缩前的CRC-32
204文件压缩后大小
244文件压缩前大小
282文件名长度(n),与对应entry的local file header中的值一致
302扩展字段长度(m),与对应entry的local file header中的值一致
322文件备注长度(k)
342文件起始位置所在的磁盘编号(这个用于zip跨磁盘的场景,在早期磁盘(软盘)是很小的,所以一个zip文件可能会跨多个磁盘,而现在基本不太可能出现这种场景了)
362内部文件属性
384外部文件属性
424本目录指向的entry相对于第一个entry的起始位置,单位byte,这也就限制了ZIP文件最大也就是4G了,再大就无法定位了
46n文件名
46+nm扩展字段
46+n+mk文件备注

EOCD

标志ZIP文件结束,判断一个文件是否是ZIP格式的文件就是读取文件末尾的EOCD来判断,而不是像其他文件一样读取文件头来判断;

offsetbytesdescription
04签名,固定值:0x06054b50
42占用磁盘数(0xffff表示是ZIP64格式)
62中央目录的起始位置所在的磁盘
82当前磁盘上的中央目录记录数
102中央目录的总数量(0xffff表示是ZIP64格式),这限制了一个ZIP32文件中存储的文件数最多不能超过65534个
124中央目录的总大小(单位byte),(0xffffffff表示这是一个ZIP64文件)
164中央目录相对于ZIP文件的起始位置(注意,是ZIP文件,不是ZIP文件的第一个entry,这是一个细微的差别,在大多数场景下都是可以忽略的,但是如果ZIP文件头包含一些前缀数据,例如自解压程序时,这个起始位置是包含这些前缀数据的,而zip的第一个entry是在这些前缀数据之后,如果不关注这个细节可能会导致ZIP解析失败)
202备注长度
22n备注

回到我们的需求

由于我们采用了第二种方式,所以我们需要能从ZIP中抽取我们需要的数据,而根据上述ZIP规范描述ZIP的中央目录设计正好能满足我们的需求,剩下的就是如何实现的问题了,下面说下大概思路(如果需要具体实现代码可以参考spring-boot-loader中的相关代码):

根据ZIP规范,我们读取一个ZIP时应该从后读取,先读取到EOCD来确定这是一个ZIP文件,同时根据EOCD来定位到中央目录,然后读取中央目录,根据中央目录构建索引,找到我们要读取的文件,由于我们是需要读取嵌套jar,所以也需要对嵌套jar构建索引,所以我们找到嵌套jar后还需要嵌套解析,最后类加载的时候根据索引来查找类数据;

这里有一个关键性的问题,当我们把索引构建完毕后,如果类加载的时候发现一个jar是在嵌套jar中,并且该嵌套jar是以压缩的方式存储在ZIP文件中,那么我们就需要对整个jar进行解压缩,否则我们是无法定位到嵌套jar中的class数据的,此时解压嵌套jar就存在两种策略了:一个就是我们将嵌套jar解压缩后缓存到内存,下次需要加载该jar中的class的时候直接从内存中取数据,可能这个嵌套jar中有几百个class,而只有两三个class是我们需要的,这样会导致内存浪费,还有一个就是我们每次需要从该嵌套jar中加载class的时候重新解压缩该嵌套jar,这就会导致我们的class加载极为耗CPU,同时也会导致类加载比较缓慢;

那么上述问题该如何解决呢?我们发现,导致上述问题的原因就是因为嵌套jar是压缩存储在ZIP中的,那么如果嵌套jar是未压缩的呢?如果嵌套jar只是存储在ZIP中,但是并未压缩,那么我们可以直接使用偏移量定位到嵌套jar中的class数据,就不会存在上述问题了,而实际上spring-boot也是这样解决这个问题的;

至此,我们的需求就解决了,而在此过程中我们也学到了很多 非(并)常(无)有(卵)用(用) 的知识;

参考文献

  • ZIP文件格式:https://en.wikipedia.org/wiki/ZIP_(file_format)


浏览 77
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报