7z密码解密_一次关于7z的踩坑经历

本文讲述了作者在处理7z密码解密时遇到的挑战,包括无法获取解压进度和效率问题。作者尝试了多种方案,如通过tar、openssl和dd改进解压流程,以及直接修改7z源码。最终,作者发现7z缺少一个命令行参数导致无法获取解压进度,添加参数后问题得以解决。然而,过程中发现原始资源包存在大量冗余文件,压缩后大小大幅减小。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 看文档一定要认真,不然你一定会后悔的!!!

这是作者纠结君的一次踩坑经历。

事情的过程有点复杂,容在下从头说起。 

缘起

话说N~久之前,纠结君的团队需要开发这样一个功能:

在 windows 操作系统的 nodejs 环境中解压一个 zip 文件,并在 nodejs 中实时获取解压进度。

对资源包的处理有如下需求:

  • 将所有资源打包成一个文件

  • 密码保护

原始资源的大小将近7G,包含的文件个数为19万,路径27万个,压缩过后的资源包大小超过了4G,所以使用7z作为解压工具。(7za是7z压缩和解压的核心,以下内容两个名词不做区别。)

这个功能是如此的 easy~ ,开发的小伙伴很快就开发完成,并进行提测了。

但是,测试小哥哥测试后说:前端进度卡在一个地方半个小时不动,这个程序肯定有 bug !

开发小伙伴弱弱地解释:这个么,你多等等就可以了。

果然,在测试小哥哥午觉醒来之后,解压成功。

但是,测试小哥哥依旧拒绝了上线请求,理由也很强大:你进度卡在那里,谁知道你后台是不是挂掉了?你还指望用户等你一个小时?

明明前端有进度条,为什么没有正常显示进度呢?

开发小伙伴说:后端拿不到7z解压进度,所以写了一个假的进度条。

对于这个理由,测试小哥哥表示理解,但对于上线依旧十动然拒。

58499c93ed9c58351d90e79b41de04f0.png

入坑

一周前,纠结君开始接手这个功能,力求让前端看到一个正常的进度。

纠结君首先尝试直接运行7z命令。发现在命令行最下方可以显示一个进度条。然后使用nodejs中的child_process.exec方法调用7z,发现在子进程的stdout中,确实获取不到这个进度条。

在查阅了7z的文档后,纠结君发现7z使用-so参数可以将解压结果输出到stdout。纠结君尝试了一下,这个输出确实可以使用nodejs获取到。

这个很不错啊,7z可以获取解压后的文件大小,把输出到控制台上的数据大小做累加,取这个累加值跟之前获取的文件大小的比值,这不就是解压进度吗?

但是,你以为这样就大功告成了吗?No, no, no!

如果你这么认为,你真是too young to simple!

程序运行完成后,在硬盘上根本找不到解压后的文件。

因为解压结果都被输出到控制台上了,根本没有写入硬盘!!!

怎么办呢?还是想办法获取解压进度吧。

为了能够获取到解压进度,作者制定了如下方案:

方案一:获取压缩包内的文件列表,根据该列表逐一解压文件。这个方案的好处是可以在node环境中控制解压进度,后期开发断点续“解压”功能很容易。

方案二:修改7z,将解压进度输出到标准输出上,让nodejs可以通过stdout或者stderr的data事件获取到解压进度。

方案一

根据文件列表逐一解压文件

这个方案的解压流程是这样的:

  • 首先使用 7z l 命令获取需要解压的文件列表

  • 然后使用 nodejs 子进程异步调用 7z 为单个文件解压(并发的子进程数量暂设为4)

首先,这个方案在逻辑上是没有问题的,但是在当前需求下是否可行,还需要进一步验证。

经过测试,使用该方案处理测试资源包(大小 45.7 M,1729 files, 2133 folders)消耗的时间为 41249.738 ms。

但是,直接使用 nodejs 异步为完整的资源包解压,消耗的时间 2539.067 ms。

生成的结果都是完整的资源文件,但为什么会出现运行效率差别这么大的情况呢?

8d478d5796655c0307a4307e680d3980.png

使用nodejs调用7z的时候,需要在nodejs运行环境中开启一个子进程,并调用shell,然后在shell中运行7z程序;7z程序在每一次解压的时候,在完成初始化之后,都需要解析zip文件的 File Header;结束解压过后,进程还需要进行资源回收等等操作。这些都是需要消耗资源的。

在解决方案中上述过程执行了1729次,而直接解压只需要执行一次,平均每次执行上述过程约需要 ( 41249.738 - 2539.067 ) / ( 1729 - 1 ) ≈ 22.402ms,而单个文件解压平均需要约( 2539.067 - 22.402 )/ 1729 = 1.456ms。

如果忽略资源包大小对上面过程速度的影响,解压需求中的文件(大小 4.22 G, 188697 files, 270840 folders)需要的时间约为:

  • 使用方案一:( 22.402 + 1.456) * 188697 = 4501933.02 ms, 即为 1.25 h;

  • 整包解压:22.402 + 1.456 * 188697 = 274765.234 ms,即为 4.5mins。

从目前的数据来看,方案一消耗的时间已经比直接整包解压所消耗的时间大了一个数量级;而经过测试,整包解压 4.22 G 文件的实际运行时间是6.4 mins。假设解压过程的运行效率跟上面的测试文件一致,那么实际做一次系统调度和信息解析的时间因为文件大小的原因变成了 109257.168 ms,即为1.8 mins,使用方案一解压所需要消耗的时间需要 (109257.168 +1.456) * 188697 = 20616774572.928 ms,即为 238.6 天。

上面的计算过程写得如此不适合人类阅读,所以作者简单总结一下:

原本只运行一次的过程

一次性跑十几万次

你不慢谁慢!

因此,这个方案虽然逻辑上可以运行,但是实际上在效率方面存在很大的问题。上面的测试和数据有一大半都是为了写这篇文章做的,实际上,在看到使用方案一解压单个文件需要的时间后,这个方案便已经被放弃了。

以上计算仅为估算,将进程并发产生的效率和消耗均摊在每个进程中,用于在数量级上对程序运行时间进行评估。实际上解压单个文件的消耗应该比上面计算的结果高。而且,计算机硬件的配置(包括CPU内核和线程个数、内存大小、磁盘类型)对实际运行效率都有巨大的影响,所以该计算结果不能作为程序的实际运行时间。

如果要对该方案进行优化,需要考虑减少单个文件解压时的重复消耗。

改进方案一:使用 tar(with gzip) + openssl + dd

这个方案需要修改原先的资源包。

打包方案流程
  • 使用tar -zcvf命令将文件打包压缩,并将结果传给 openssl

  • openssl使用aes256进行加密,并使用dd写入磁盘

解压方案流程

与打包方案正好相反

  • 使用dd读取压缩包,传给openssl进行解密

  • 将解密的结果使用tar -zxvf进行拆包和解压

该方案使用 tar 将资源进行归档,并在归档时调用gzip对单个文件进行压缩; 使用 openssl 作为加密层,dd 作为读写层,避免大文件读写带来的问题。

因为使用gzip对单个文件进行压缩和解压,大大缩短了解析单个文件时的消耗。

不过这个方案依旧存在一个问题:openssl 加密后的文件无法使用 tar 事先提取有关信息。

改进方案二:使用zip对单个文件进行压缩和保护,使用tar进行归档

跟上面的方案相比,这个方案将加密和压缩放在了 zip 中,解决了上面先打包后加密造成的 tar 无法提取文件信息的问题。

然而,作者这个爱作死的家伙对 7z 不死心,还是决定试试方案二。

方案二

将进度输出到标准输出

其实,原则上说,要实现将进度输出到标准输出,最快的方式应该是从 7z 的帮助文档里寻找相关配置项。但是,大概是因为作者的眼神不好,并没有在文档里找到这一项配置,于是才有了方案一的产生。

然而,在作者想办法将进度条输出到标准输出到时候,不知为何,脑子一抽,没有重新查看帮助文档,而是决定去翻看 7z 的源代码。

 7z 源码中使用一个叫做 PercentPrinter 的类管理解压进度的输出,对外暴露的方法除了构造函数和析构函数之外,还有两个方法,一个是 ClosePrint, 一个是 Print。其中,ClosePrint 是用来控制输出方式的,Print 是用来生成打印内容的。

这个类调用了一个叫做 StdOutStream 的类用于管理输出流,这个流封装了 FILE 和 stdout 两个流,进度条使用的是 stdout。

因为进度条是单行输出,所以在处理输出的时候,PercentPrinter里面使用了 ‘\b’ 和 ‘\r’ 进行辅助处理。为了确认是否是单行输出造成的问题,作者写了一个小Demo。

这个是入口函数,用于控制输出。

c51e4c5f0872256573f17dfc3f75f40c.png

下面两个函数都可以实现单行输出。

2521ad6fad176bd77f7bb3a9dbcc1eed.png

271ff01ac7685a64a6c8d3c957a48e1b.png

但是,使用 node 调用这个demo生成的文件的时候,两个输出方案都可以正常从子进程获取到输出。

f788906f77bf8937a98c15da6a9afd24.png

这说明,7z 不能从 node 上获取进度条,跟单行输出无关。

然后,作者尝试在 7z 运行时的各个时机使用传统的 prinrf 和 c++ 标准的std::cout 输出,发现 7z 做环境配置的时候,所有输出都是正常的,但是在开始解压后,所有的输出都不能被获取。

全局搜索代码,没有发现输出流被修改,因此可以做一个猜想,应该是 7z 在解压前运行了某个配置,使得 Print 没有运行。

6e1bdc8149e458549fbef47786aaf1bc.png

想要找到这个配置,就需要接着看源码。

考虑到管理输出流的 StdOutStream 是一个通用工具类,所以相关的配置应该位于PercentPrinter以及调用它 Print 方法的地方。

f76be0d976f07d02179aa28398f57ad8.png

在全局搜索 Print 之后,作者发现了几个调用这个方法的位置,检查上下文后发现,程序在调用Print前都需要进行一个判断:

acc0ba39dec91dcf0cefbef4a7eae905.png

而_percent是PercentPrinter的实例,这个实例存在于几个callback类中,这几个类使用_percent对象的方法是一样的:

  • 在初始化的时候为_percent对象设置输出流

81d43ad081252c7b47fa7d79c0e8520f.png

  • 在适当的时机调用Print方法

而这几个类会在Main中被初始化。

c9bbe16caffdd482c89d65738f131ace.png

而传入的参数percentsStream也在Main中被初始化。

00c7cccb0089c2392f2d5f7881daeba1.png

也就是说,控制台最终是否会输出到标准输出,完全取决于上图中第524行是否执行。作者手动令这行命令强制执行,运行结果确实验证了作者的想法。

这行代码的执行条件是:

options.Number_for_Percents != k_OutStream_disabled

其中, k_OutStream_disabled 是一个等于0的枚举值。

904cab581ff2337cbafc0bc0643b75ad.png

而 options 是名为CArcCmdLineOptions的类的实例,而这个类是用来处理控制台输入的!!!也就是说,作者之前折腾了这么久的东西,其实根本原因是7z运行的时候少写了一个参数!!!

此时,灰心失望的作者默默回到terminal,调出帮助手册,在某一个角落找到了这个配置。

9bd059616d8b251ec92477590041a359.png

然后默默回到测试程序,在运行的命令后添加参数 -bsp1。

终于,天下太平了。

结局

结局是用来反转的

如果你认为作者最开始的项目就这样搞定了,那你就错了。

作者和小伙伴在一起商量解决方案的时候,发现一件事情:初始的资源包有很多冗余文件。

多到什么程度呢?

多到把多余的文件删掉后,压缩包的大小不到1G!!!

95be9811c4b0e892d1ffd3f89b1aeacd.png

好了,就这样结束吧,作者已经哭晕在墙角。


30e65740d130e0697689d970a57118d5.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值