【问题标题】:How do I decompress a MSZIP block?如何解压缩 MSZIP 块?
【发布时间】:2021-10-12 02:13:58
【问题描述】:

我有一个压缩文件,它是一个 CAB,我希望从 Linux 中提取一个文件。由于 Linux 上没有任何本地 CAB 提取器,我想我会尝试完成一个。

虽然我已经看过 MSZIP 文档 [0] 以及 [1] 和 [2],但我在解压缩它时遇到了困难,即使每个 Block 都是使用修改后的 DEFLATE 压缩器压缩的。虽然我确实使用 [3] 来解压缩块,但除了第一个块之外,其余块有很多丢失的数据 [完全无效的数据 [即0x00 填充]。

所以我相信我需要手动解决。

很遗憾,我无法理解 [2]。所以, 使用以下数据:

ED 9D 79 70 1C C7 75 87 07 E0 4D 88 24 EE 8B 94
04 1E A2 44 51 04 81 05 08 2D 11 4A C2 5E 00 96
...

以及下面的代码来剖析数据:

#!/bin/env python

import os


def to_bin(in_item):
    b = ord(chr(in_item))
    retval = bin(b).replace("0b", "")
    if len(retval) < 8:
        lx = 8 - len(retval)
        q = "0" * lx
        retval = q + retval
    return retval


def swapbytes(in_bit_str):
    p = in_bit_str[:4]
    q = in_bit_str[4:]
    return q + p


pth = os.path.join("comp", "test_0.dat")

fp = open(pth, 'rb')
fpd = fp.read(10)
fp.close()

dx = ""
for item in fpd:
    cx = to_bin(item)
    scx = swapbytes(cx)
    dx += scx
    print(cx)
    print("---> %s" % scx)

print("Final: %s" % dx)

bfinal = dx[0]
btype = dx[1:3]

print("Bfinal: %s" % bfinal)
print("Btype: %s" % btype)

rest = dx[3:]

hlit = rest[:5]
hdist = rest[5:11]
hclen = rest[11:15]

hlitd = int(hlit, 2)
hdistd = int(hdist, 2)
hclend = int(hclen, 2)
print("HLIT: %s [%d]" % (hlit, hlitd))
print("HDIST: %s [%d]" % (hdist, hdistd))
print("HCLEN: %s [%d]" % (hclen, hclend))

我得到以下输出:

$ python tstdcmp.py
11101101
---> 11011110
10011101
---> 11011001
01111001
---> 10010111
01110000
---> 00000111
00011100
---> 11000001
11000111
---> 01111100
01110101
---> 01010111
10000111
---> 01111000
00000111
---> 01110000
11100000
---> 00001110
Final: 1101111011011001100101110000011111000001011111000101011101111000011100000
0001110
Bfinal: 1
Btype: 10
HLIT: 11110 [30]
HDIST: 11011 [27]
HCLEN: 0011 [3]

所以我从 RFC1951 [2] 中得到的基本概念是,在一个字节中,它是从最高位到最低位。即 7 6 5 4 3 2 1 0 但是对于多个字节,即 AB CD,字节被交换到 CD AB。

所以通过代码,我确定前两个字节ED 9D 实际上应该是9D ED

并且以位为单位: 11011110 11011001

这意味着 BFinal == 1 和 BTYPE == 10(因此是动态霍夫曼编码)。

删除前三位后,我得到: 11110 11011001

所以从 [2] 的 3.2.7 开始,我应该有以下几组位的以下信息:


 5 Bits: HLIT, # of Literal/Length codes - 257 (257 - 286)
 5 Bits: HDIST, # of Distance codes - 1        (1 - 32)
 4 Bits: HCLEN, # of Code Length codes - 4     (4 - 19)

所以,HLIT = 11110 [30] HDIST = 11011 [27] HCLEN = 0011 [3]

但是从上面看,HLIT 应该在 257 到 286 之间 和 HCLEN 在 4 和 9 之间;但我得到 HLIT = 30 和 HCLEN = 3。

也就是说,3.2.7 中的文档:

For even greater compactness, the code length sequences
themselves are compressed using a Huffman code.

那么这是用霍夫曼代码进一步压缩的例子吗? 我曾希望了解这个 RFC1951[2];但这非常令人困惑。

我看过一些关于 DEFLATE 压缩的 Youtube 视频;但它们主要展示的是算法,而不是字节是如何打包到文件中的。

非常感谢任何帮助。

谢谢

[0] - https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-mci/27f0a9bf-9567-4e40-ad66-6ae9ab9d2786

[1] - How to decompress MSZIP files with c#?

[2] - https://datatracker.ietf.org/doc/html/rfc1951

[3] - https://www.nayuki.io/page/simple-deflate-decompressor

【问题讨论】:

    标签: python compression deflate


    【解决方案1】:
    1. 不,您认为从 RFC 1951 获得的“基本概念”是完全错误的。首先,从最低有效位到最高有效位读取每个字节中的位。其次,您不要反转流中的字节。前八位首先从第一个字节读取,后八位从第二个字节读取,依此类推。 (存储块中的长度以 little-endian 存储,但这既不反转也不反转。这只是 16 位长度在字节流中序列化的方式。)
    2. 一旦正确读取位,HLIT 等值将完全按照 RFC 中的说明进行存储。前五位是文字/长度代码的数量减去 257。所以你取这五个位的值,它给出了一个 0..31 的数字,然后加上 257。这给出了 257..288 范围内的数字。如同一行所述,允许的范围实际上是 257..286,因此 5 位的最后两个可能值 30 和 31 不应出现在有效的 deflate 流中。
    3. RFC 1951 一点也不令人困惑。这是对格式的清晰完整的描述。但是,您需要有足够的压缩背景知识,尤其是 Huffman 代码,才能理解它。 RFC 并非旨在成为一本关于压缩的教科书,也不是一本关于如何以位编码整数的教科书。
    4. 很明显,您需要一些时间才能弄清楚这一切。幸运的是,您不需要编写自己的充气机。您可以改用zlib。阅读 zlib.h 中的文档以了解所有 inflate 函数。
    5. 在 CAB 文件中,MSZIP CFDATA 块使用先前 CFDATA 块的历史记录,直到达到文件夹边界。即使每个块都是正确终止的放气流,下一个块也可以引用来自前一个块的未压缩数据。要在第一个之后处理 CFDATA 块,您需要使用 zlib 的 inflateResetKeep() 函数来重新启动膨胀过程,同时保留之前膨胀操作的字典。

    作为参考,这里是您提供的 deflate 流的初始字节的解码,使用 infgen

    ! infgen 2.5 output
    !
    last            ! 1
    dynamic         ! 10
    count 286 30 16     ! 1100 11101 11101
    code 16 4       ! 100
    code 17 7       ! 111
    code 0 4        ! 100 000
    code 8 3        ! 011
    code 7 4        ! 100
    code 9 3        ! 011
    code 6 4        ! 100
    code 10 3       ! 011
    code 5 4        ! 100
    code 11 3       ! 011
    code 4 5        ! 101
    code 12 3       ! 011
    code 3 7        ! 111
    code 2 6        ! 110 000
    lens 6          ! 1100
    lens 8          ! 000
    lens 8          ! 000
    lens 9          ! 001
    repeat 6        ! 11 1110
    lens 9          ! 001
    lens 8          ! 000
    lens 10         ! 010
    lens 9          ! 001
    lens 9          ! 001
    lens 9          ! 001
    lens 8          ! 000
    repeat 6        ! 11 1110
    repeat 4        ! 01 1110
    lens 9          ! 001
    lens 9          ! 001
    lens 10         ! 010
    lens 10         ! 010
    lens 10         ! 010
    lens 8          ! 000
    lens 9          ! 001
    repeat 3        ! 00 1110
    lens 10         ! 010
    lens 9          ! 001
    lens 10         ! 010
    lens 10         ! 010
    lens 9          ! 001
    lens 10         ! 010
    lens 9          ! 001
    lens 10         ! 010
    lens 9          ! 001
    lens 8          ! 000
    lens 9          ! 001
    lens 8          ! 000
    lens 8          ! 000
    lens 7          ! 1101
    lens 8          ! 000
    lens 8          ! 000
    lens 9          ! 001
    lens 8          ! 000
    lens 10         ! 010
    lens 7          ! 1101
    lens 9          ! 001
    lens 8          ! 000
    lens 12         ! 100
    lens 9          ! 001
    lens 10         ! 010
    lens 10         ! 010
    lens 10         ! 010
    lens 8          ! 000
    lens 7          ! 1101
    repeat 4        ! 01 1110
    lens 8          ! 000
    lens 8          ! 000
    lens 8          ! 000
    lens 7          ! 1101
    lens 9          ! 001
    

    【讨论】:

    • 感谢您指出我哪里出错了。我很抱歉像笨蛋一样混乱。澄清一下,在我的示例中,ED(即 11101101)实际上是 10110111?
    • 没有。 ED 始终是 11101101。但是,您开始从 bottom (最不重要)而不是顶部读取位。这样,当两个或多个位表示一个值时,您可以按正确的顺序获得位。该字节被分解为11101101。首先是底部的1。那是“最后一个”位,表示这个第一个块也是放气流中的最后一个块。然后下一个是10,表示一个动态代码块。下一个是11101 = 29. 29 + 257 = 286,表示这个block的header中有286个literal/length码长。
    • 请注意我在答案中的第 4 点和第 5 点。只需使用 zlib 和 inflateResetKeep() 函数将 MSZIP 压缩数据提取到 CAB 文件中。
    • Python 的decompressobj() 有一个可选的字典参数,您可以使用它。只需保留前一个 CFDATA 块中未压缩数据的最后 32K(或全部,如果小于 32K),并将其作为下一个块的字典提供。
    • 非常感谢!!这就像一个魅力!非常感谢您的帮助!
    猜你喜欢
    • 2017-01-16
    • 2021-01-19
    • 2012-11-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-22
    • 1970-01-01
    • 2011-01-26
    • 1970-01-01
    相关资源
    最近更新 更多