ZIP(32位)文件格式详解

ZIP(32位)文件格式详解

为什么要去了解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文件第一个entry的起始位置(注意,不是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也是这样解决这个问题的;

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

参考文献

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
7-ZIP32.DLL 是一个供第三方软件调用进行压缩7z 或解压7z 的动态链接库。 2014/6/25 Version 9.22.00.01 新增的API。 (SevenZipGetLastError) 新增的API。 (SevenZipSfxConfigDialog) 新增的API。 (SevenZipSfxFileStoring) 支持长路径名。 (仅适用于基于NT的,在某种程度上是7-zip的本家兼容) 更改为返回FALSE,如果密码是错误指定CHECKARCHIVE_FULLCRC标志SevenZipCheckArchive。 加强错误处理。固定在没有返回现场应该返回一些错误。 定,如果缓冲区耗尽SevenZipGetArcFileName等来返回一个错误。 SevenZipGetMethod的略作修改的规范。相应的缓冲区不足的时间变化。 支持M_CHECK_FILENAME_ONLY国旗和M_CHECK_ALL_PATH在SevenZipOpenArchive。固定M_CHECK_FILENAME_ONLY的(“-R-”),是一个M_CHECK_ALL_PATH(“-R”)的默认状态。还可以支持(“-R0”)通过使用在同一时间的两个标志。 修正了无法显示的密码窗口时没有任何窗口的应用程序处理加密文件中的错误。 固定有在那里当然对话框的处理速度的项目显示100倍的情况。 修正了SevenZipGetArcAccessTimeEx必须得到更新的日期和时间的错误。 未安装-SLP到非支持的DLL,就像因此不能使用开关,一旦7-Zip文件管理器。 (您可以使用) 重建在7-ZIP9.22。 - 我可以在MF= FilterID开关指定压缩过滤器。下面的例子。 A-MF= BCJ2 a.7z a.tar A-MF=三角洲:4 a.7z a.wav A-MF= BCJ a.tar.xz a.tar - 我可以用最好的4GB内存在64位版本的Windows。 - 修正了几个错误。...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值