zlib.gzip在不同的操作系统上为相同的输入生成不同的结果

时间:2014-10-22 20:19:07

标签: javascript node.js operating-system

以下代码(在节点js v0.10.28上):

var zlib = require('zlib');
var buf = new Buffer('uncompressed');

zlib.gzip(buf, function (err, result) {
 console.log(result.toString('base64'));
});

生成字符串:

Win 7 x64上的

H4sIAAAAAAAACyvNS87PLShKLS5OTQEA3a5CsQwAAAA=

^ ^

Mac上的

H4sIAAAAAAAAAyvNS87PLShKLS5OTQEA3a5CsQwAAAA

^ ^

CentOs上的

(Linux 2.6.32-279.19.1.el6.x86_64)

H4sIAAAAAAAAAyvNS87PLShKLS5OTQEA3a5CsQwAAAA=

^ ^

它们似乎在结尾=和第13个字符(C vs A)上有所不同,但我不确定原因。

1 个答案:

答案 0 :(得分:4)

一般的差异的一些理论来源

gzipRFC 1952)使用deflateRFC 1951)作为压缩格式,从技术上讲,它只是一种文件格式规范。特别是,算法在如何选择压缩他们给出的字节方面给予了很大的自由度(这实际上是一种强度)。

可以与deflate一起使用的两种基本压缩机制:

  • 长度受限的霍夫曼编码:更频繁出现的字符可以被赋予更短的比特序列,并且更少频率的字符可以被赋予更长的比特序列,导致总体上更少的比特来表示相同的信息。用于编码的霍夫曼树可以从输入(或其一部分)动态计算,或者可以固定。因此,不同的编码器可能会对同一输入使用不同的霍夫曼树,从而导致树本身和编码字符的不同表示。

  • LZ77压缩:先前已输出的子串不需要再次输出;相反,只需输出具有相同子串长度的反向引用。由于在给定输入中找到所有公共子串是Hard Problem™,因此通过给定的启发式(例如,跟踪以每个双字符前缀开头的最后6个子串)来寻找尽可能多的子串通常更有效率。 。同样,不同的编码器可以(有效地)为同一输入产生不同的输出。

最后,所有这些压缩数据都分散到一个或多个中,并且在切换到新块时由编码器自行决定。从理论上讲,这甚至可以在每个字节完成(虽然这不会真的是压缩!)。当结束块时,因为其内容是使用霍夫曼位代码编码的,所以该块可能不会在字节边界上结束;在这种情况下,如果流中的后续项必须从整个字节开始(例如,必须从字节边界开始),则可以添加任意位作为填充以舍入到下一个字节。

正如您所看到的,相同输入的压缩字节可能有很多不同之处!即使使用相同的算法(例如规范zlib library,也不要与同名的RFC(1950)混淆),不同的压缩级别通常会导致不同的结果。甚至可以想象,相同的程序在具有相同输入和选项的相同环境中多次运行会产生不同的结果,例如,由于数据结构命令指针或使用指针作为哈希值 - 指针值可以在执行之间改变。此外,多线程实现本质上往往是不确定的。简而言之,除非您明确使用的实现提供了该保证,否则您不应该依赖于给定输入的输出相同。 (虽然大多数理智的实施都力求确定性,但在技术上并不需要。)

为什么您的特定示例Base64字符串不同

将尾随=符号的差异暂时搁置一分钟,三个示例中的两个具有完全相同的表示。这两个在第十个字节的第一部分中恰好相差一位(C - > A)(Base64将三字节编码为base-64字符的四元组,因此第十三个Base64字符是第十个字节的前六位)。 A代表0,C代表2 - 但请记住,这是字节的高6位,所以它真的是0和8加上低2位。那两个低位是下一个Base64字符的高两位yy表示50,二进制是110010,所以第十个字节的低两位是{将它组合在一起,第十个字节是不同的字节,其值从一个实现开始为11,另一个实现为3。快速查看0b11 RFC会发现第十个字节表示执行编码的操作系统/文件系统:果然,11被定义为" NTFS文件系统( NT)",和3#" Unix"。因此,这种情况的不同完全是由于您执行编码的操作系统。 (请注意,任何gzip文件的第二个dword是时间戳,在您的示例中设置为0(无可用),但在所有三个试验中可能很容易产生差异,这使得差异更难发现。)

至于尾随gzip,那只是Base64的填充(explained nicely on Wikipedia)。由于Base64采用三个字节的组并使用四个字节对它们进行编码,如果编码的字节数不能被三整除,则使用最小的Base64数字(将输入结束后的字节视为空字节):对于单个字节字节,只需要两个Base64数字;对于两个,只需要三个。仅添加=个符号以将Base64数字的数量四舍五入到四个数字;你会注意到这意味着为解码Base64字符串并不真正需要=符号,因为你知道它的长度(但是如果它不是一个字符串,那么一些Base64解码器会拒绝该字符串长度为4的倍数。因此,您的第二个和第三个示例表示完全相同的字节值,但由不同的Base64编码器生成。

你有它!我认为我的答案对于几乎归结为一句话答案的技巧问题而言过于冗长,但我无法拒绝详细解释所有内容: - )