漏水抽象

2021-06-03 23:48:06

在20世纪90年代末,Windows Shell和Internet Explorer团队介绍了许多辉煌和复杂的设计,允许错综复杂的shell和浏览器来处理超出Microsoft本身建造的场景。例如,Internet Explorer支持可插拔协议的概念(如果某些协议,例如,FTPS与HTTP?“变得同样重要的话),并且Windows Shell提供了一个非常灵活的抽象浏览名称空间,使第三方能够构建可浏览“文件夹”未由文件系统备份 - 从WebDAV(“您的HTTP-Server是文件夹”)到CAB文件夹(“您的CAB存档是文件夹”)的所有内容。作为2004年的Clipart团队的PM,我建立了基于.NET的应用程序来从Office Web服务浏览剪贴画,我勾勒出了一个初始设计,以使其看起来像Microsoft的基于Web的Web Clipart存档安装在系统上的本地文件夹中。

也许最受欢迎的(或臭名昭着)的shell命名空间扩展名称是压缩文件夹扩展,处理ZIP文件的探索。首先在Windows 98 Plus包中引入,后来包含在Windows Me +直接,压缩文件夹允许数十亿个Windows用户与Zip文件进行交互,而无需下载第三方软件。也许令人惊讶的是,该特征本身就是从两个第三方获得的 - 微软从Dave Plummer的“侧面项目”中获得了探险家集成,而一家名为InnerMedia索赔的公司,为下面的“Dynazip”引擎。

不幸的是,代码暂时没有更新。很久了。模块中的时间戳声称它最后一次更新了1998年的情人节,而我怀疑在这里或之后可能已经解决了(和一个功能,只有unicode文件名支持),而代码则没有秘密是的,正如Raymond Chen所说:“陷入世纪之交。”这意味着它不支持像AES加密这样的“现代”功能,并且已知其性能(运行时,压缩比)被显着逊于现代的第三方实现。

那么,为什么它没有更新?好吧,“如果它是一个破坏,不要修复它”占思考的一部分 - Zip文件夹的实施已经在Windows中幸存了23年,而没有客户令人难以忍受的嚎叫,因此有一些证据表明用户足够高兴。

不幸的是,ZIP文件夹支持真正被破坏的堕落情况。我昨天跑过其中一个。我已经看到了一个关于十六进制编辑的有趣的推特线程,提供注释(有助于探索文件格式)并决定尝试几次(我决定最喜欢Rehex)。但在此过程中,我下载了IMHEX的便携式版本,并试图将其移动到我的工具文件夹中。

我通过双击11.5MB Zip打开它来完成此操作。然后,我按Ctrl + A选择所有文件,然后粗略(SPOILER ALERT)CTRL + X将文件剪切到我的剪贴板。

然后,我在C:\ Tools文件夹中创建了一个新的子文件夹,并按住Ctrl + V粘贴。在这里,一切都在轨道上越来越多地在一分钟内花了“计算......”,除了在单个5k文件中创建单个子文件夹之外,没有明显的进展:

呵呵?我知道拉链文件夹下方的拉链发动机并不优化,但我以前从未见过这么糟糕。等待几分钟后,另一个文件提取,这是6.5 MB:

这是香蕉。我打开了任务管理器,但似乎没有什么可以使用我的大部分线程CPU,我的64GB内存或我的NVME SSD。最后,我打开了Sysinternals的过程监视器,试图看看发生了什么,并且很快就会看到问题的根本原因。

在文件末尾(ZIP文件保留其索引的位置)之后,一次从磁盘读取整个1100万字节文件:

仔细观察,我意识到读取几乎都是一个字节,但是,现在,然后,在特定的1字节读取之后,发出了15个字节的读取:

那些有趣的抵消是什么(330,337)?字节0x50,AKA字母P.

在过去编写了一些琐碎的拉链恢复代码,我知道zip文件中的字符p的特殊性是什么 - 它是zip格式的块标记的第一个字节,每个字符串都以0x50 0x4b开头。因此,这里显然发生了什么,代码正在从开始读取文件,以完成一个特定的块,大小为16个字节。每次击中P时,它都会查看接下来的15个字节,以查看它们是否与所需的签名匹配,如果没有,它会继续扫描Byte-Byte,查找下一个P.

邮政局格式由一系列文件记录组成,然后是这些文件记录的列表(“中央目录”)。

每个文件记录都有自己的“本地文件标题”,其中包含有关文件的信息,包括其大小,压缩大小和CRC-32;中央目录中重复相同的数据。

但是,zip格式允许本地文件头省略此数据,而是在压缩数据之后将其写入“挂步”,这是在流式压缩时有用的功能 - 在您实际完成之前,无法知道最终压缩大小压缩数据。大多数zip文件可能不使用此选项,但我的示例下载它是。您可以看到CRC和尺寸在标题中为0',而是在签名0x08074b50(数据描述符)之后立即显示在下一个文件的本地标题之前:

通用标志中的0x08位表示此选项; 7-zip的用户可以在条目特征列中找到它作为描述符:

基于读取大小(1 + 15个字节),我假设代码是针对数据描述符块的刨花。为什么它这样做(与读取来自中心目录的相同数据)我不知道。

使事情变得更糟,这就是“读取文件,字节”爬行通过文件爬网并不只是发生一次,因为每个文件都至少发生一次。更糟糕的是,使用ReadFile而不是Fread()读取此数据。

用符号重新启动和配置进程监视器后,我们可以检查一个字节读取并获得一丝暗示正在进行的内容:

GetSomebytes函数正在击中传递单个字节缓冲区的呼叫,在Readzipfile功能内的紧密循环中。但是瞧不起堆栈,乱七八糟的根本原因变得清晰 - 这发生了,因为在每个文件从zip到目标文件夹后,必须更新zip文件以删除“移动”的文件。此删除过程本质上不是快速(因为它导致将文件的所有后续字节进行洗牌并更新索引),并且如在ReadzzipFile函数中实现(具有其单字节读缓冲区)更慢。

备份我的repro步骤,请注意,我按Ctrl + X键“剪切”文件,从而导致移动操作。如果我替代地击中Ctrl + C要“复制”文件,导致复制操作,ZIP文件夹将不执行删除操作,因为提取了每个文件。解压缩ZIP文件所需的时间从30分钟到四秒钟下降。对于透视图,7-zip将文件解压缩在一秒钟内的一秒钟内,尽管它欺骗了一点。

这里是从用户的视图泄露的地方,从zip文件中复制文件(然后删除zip)与zip文件中的文件似乎不应该是非常不同的。遗憾的是,抽象无法通过删除某些ZIP文件的现实中的现实中的完全纸张,同时从磁盘中删除文件通常是微不足道的。因此,压缩文件夹抽象适用于微小的拉链,但失败的较大的ZIP文件变得越来越普遍。

虽然考虑到大大提高这种情况的性能的方法相对容易,但是先例表明Windows中的代码不太可能随时改善。也许是它的25周年纪念日? 🤞