Zip文件:历史记录,说明和实现



我一直想知道如何压缩数据,包括Zip文件中的数据。一旦我决定满足我的好奇心:学习压缩的工作原理,并编写自己的Zip程序。在编程中,实现已成为令人兴奋的练习。创建一个调试的机器以获取数据,将其传输到更有效的表示形式,然后再收集回去,您会感到非常高兴。我希望您也对阅读它感兴趣。

本文将详细介绍Zip文件和压缩方案的工作原理:LZ77压缩,霍夫曼算法,Deflate算法等。您将学习该技术的发展历史,并查看用C从头开始编写的相当有效的实现示例。源代码在这里:hwzip-1.0.zip

我非常感谢Ange AlbertiniGynvael ColdwindFabian GiesenJonas Skeppstedtweb),Primiano TucciNico Weber,他们对本文的草稿提供了宝贵的反馈意见。

内容



故事


邮编


在80年代和90年代初,在Internet普及之前,计算机发烧友使用拨号调制解调器通过电话网络连接到Bulletin Board Systems(BBS)网络。BBS是一个交互式计算机系统,允许用户发送消息,玩游戏和共享文件。要上网,拥有一台计算机,调制解调器和一个不错的BBS电话号码就足够了。这些数字已在计算机杂志和其他BBS上发布。归档程序

是促进文件分发的重要工具它允许您将一个或多个文件保存在一个存档文件中以更方便地存储或传输信息。理想情况下,归档文件还压缩文件以节省空间和时间,以便通过网络传输。在BBS时代,Arc存档器很受欢迎,它是由System Enhancement Associates(SEA)的Tom Henderson撰写的,这是他与他的妹夫创立的一家小公司。

在1980年代后期,程序员Phil Katz发行了自己的Arc版本PKArc。它与SEA Arc兼容,但是由于以汇编语言编写的子例程并使用了新的压缩方法,因此工作速度更快。该程序广受欢迎,Katz辞去工作,创建了PKWare以专注于进一步的开发。据传说,大部分工作都在他母亲在威斯康星州格伦代尔的厨房里进行。


1994年9月19日,密尔沃基哨兵报上的文章, Phil Katz摄

然而,SEA对Katz的倡议并不满意。该公司指控他侵犯商标和版权。 BBS网络和PC世界上的诉讼和争议被称为“ 电弧战争”Arc Wars)。最后,争端得以解决,并获得了SEA的青睐。

Katz放弃了Arc,于1989年创建了一种新的归档格式,他将其称为Zip并向公众公开

, , , . , ".ZIP", , , , , , , , , .

Katz创建此类文件的程序称为PKZip,并很快传播到BBS和PC领域。

Zip格式成功的最可能方面之一是PKZip随附的应用程序说明文档,其中详细说明了该格式的工作原理。这使其他人可以学习格式并创建可以生成,提取或与Zip文件进行交互的程序。

压缩-一种压缩格式,不会丢失:解压缩后的数据将与压缩前相同。该算法在源数据中寻求冗余,并更有效地呈现信息。这种方法不同于有损压缩。,以JPEG和MP3等格式使用:压缩后,会丢弃掉一些人眼或耳不易察觉的信息。

PKZip作为共享软件分发:可以免费使用和复制,但作者建议用户“注册”该程序。只需47美元,您就可以获取印刷版说明,高级支持以及该应用程序的增强版。


PKZip的关键版本之一是2.04c,该版本于1992年12月28日发布(版本2.04g在不久之后发布)。它使用默认的Deflate压缩算法。该版本确定了Zip文件中压缩的进一步发展(专门针对该版本的文章)。


从那时起,zip格式已用于许多其他文件格式。例如,Java存档(.jar),Android应用程序包(.apk)和Microsoft Office .docx文件使用Zip格式。许多格式和协议使用相同的压缩算法Deflate。假设网页可能以gzip文件的形式传输到浏览器,其格式使用Deflate压缩。

菲尔·卡兹(Phil Katz)于2000年去世。尽管PKWare公司主要致力于数据保护软件,但它仍然存在并支持Zip格式。

信息zip和zlib


在1989年PKZip发布之后不久,用于解压缩Zip文件的其他程序开始出现。例如,可以在Unix系统上解压缩的解压缩程序。 1990年3月,创建了一个名为Info-ZIP的邮件列表。Info-ZIP

已发布了免费的开源程序unzipzip,这些程序用于解压缩和创建zip文件。该代码已被移植到许多系统,并且仍然是Unix系统的Zip程序的标准。后来,这有助于提高Zip文件的知名度。 将进行压缩压缩和解压缩的Info-ZIP代码移至他们编写的单独的zlib

让-卢普·盖伊(压缩)和马克·阿德勒(拆箱)。


Jean-loup Gailly(左)和Mark Adler(右)在2009年USENIX STUG奖上

创建该库的原因之一是,它提供了在其他应用程序和格式(例如新gzipPNG)中使用Deflate压缩的便利。这些新格式旨在替代使用专利保护的LZW算法的CompressGIF

作为创建这些格式的一部分,Peter Deutsch编写了Deflate规范,并于1996 5月Internet RFC 1951的名称发布了该规范。与原始的PKZip应用笔记相比,这是一个更易于访问的描述。

今天,zlib随处可见。也许他现在负责在Web服务器上压缩此页面并将其在浏览器中解压缩。如今,大多数zip文件都是使用zlib压缩和解压缩的。

Winzip


许多没有找到PKZip的人都使用WinZip。 PC用户从DOS切换到Windows,从PKZip切换到WinZip。

一切始于程序员Nico Mac的一个项目,该程序员在康涅狄格州Storrs-Mansfield的Mansfield Software Group创建了OS / 2软件。 Nico使用了Presentation Manager,这是OS / 2中的图形用户界面,他为每次创建Zip文件都不得不从文件管理器切换到DOS命令而感到不安。

Mac编写了一个简单的GUI程序,直接在Presentation Manager中使用Zip文件,将其命名为PMZip,并在1990年代作为共享软件发布。

OS / 2没有成功,PC世界接管了Microsoft Windows。1991年,Mac决定学习如何编写Windows程序,他的第一个项目是将Zip应用程序移植到新的OS。1991年4月,发布了WinZip 1.00它以共享软件的形式分发,试用期为21天,注册费为29美元。她看起来像这样:


在WinZip的第一个版本中,引擎盖下使用了PKZip。但是从1993年的5.0版开始,Info-ZIP的代码开始用于Zip文件的直接处理。用户界面也逐渐发展。


Windows 3.11 for Workgroups下的WinZip 6.3

WinZip是1990年代最受欢迎的共享软件程序之一。但是最后,由于在操作系统中嵌入对Zip文件的支持,它失去了相关性。Windows从2001年开始使用它们作为“压缩文件夹”(Windows XP),为此使用 DynaZip

Mac最初称为Nico Mak Computing。在2000年,它被更名为WinZip Computing,而在那之后的大约一年,Mack离开了它。2005年, Vector Capital出售了该公司,最终归 Corel所有,后者仍将WinZip作为产品发布。

Lempel-Ziv压缩(LZ77)


Zip压缩包含两个主要成分:Lempel-Ziv压缩和Huffman代码。

压缩文本的一种方法是创建常用单词或短语的列表,并用指向字典的链接替换文本中这些单词的变体。例如,源文本中的长单词“ compression”可以表示为#1234,其中1234表示单词在列表中的位置。这称为字典压缩

但是从通用压缩的角度来看,该方法有几个缺点。首先,到底应该把什么纳入字典?源数据可以使用不同的语言,甚至可以不是人类可读的文本。而且,如果在压缩和解压缩之间未事先约定字典,则必须将其与压缩数据一起存储和传输,从而降低了压缩的好处。

一个很好的解决方案是将源数据本身用作字典。在1977年,一种通用算法顺序数据压缩的,雅各布谢夫和亚伯拉罕的Lempel(谁在Technion工业公司制作),提出了在源数据被呈现为三元组的序列的压缩方案:

(, , )

在那里,他们形成一个反向链接到您要在原文的上一个位置复制的字符序列,而这是在生成的数据的下一个字符。


亚伯拉罕·伦佩尔(Abraham Lempel)和雅各布·齐夫(Jacob Ziv)。

考虑以下几行:

It was the best of times,
it was the worst of times,

在第二行中,序列“ t是w”可以表示为(26,10,w),因为它是通过将10个字符从26个字符的位置复制回字母“ w”来重新创建的。对于尚未出现的字符,使用零长度反向链接。例如,初始“ I”可以表示为(0,0,I)。

此方案称为Lempel-Ziv压缩或LZ77压缩。但是,在该算法的实际实现中,通常不使用三元组的一部分。相反,字符是单独生成的,并且(对用于反向链接(此选项称为LZSS压缩)。如何文字和反向链接被编码为一个单独的问题,我们会考虑它,当我们分析的算法如下放气

本文:

It was the best of times,
it was the worst of times,
it was the age of wisdom,
it was the age of foolishness,
it was the epoch of belief,
it was the epoch of incredulity,
it was the season of Light,
it was the season of Darkness,
it was the spring of hope,
it was the winter of despair,
we had everything before us,
we had nothing before us,
we were all going direct to Heaven,
we were all going direct the other way

您可以这样压缩:

It was the best of times,
i(26,10)wor(27,24)age(25,4)wisdom(26,20)
foolishnes(57,14)epoch(33,4)belief(28,22)incredulity
(33,13)season(34,4)Light(28,23)Dark(120,17)
spring(31,4)hope(231,14)inter(27,4)despair,
we had everyth(57,4)before us(29,9)no(26,20)
we(12,3)all go(29,4)direct to Heaven
(36,28)(139,3)(83,3)(138,3)way

反向链接的重要属性之一是它们可以重叠。当长度大于距离时,会发生这种情况。例如:

Fa-la-la-la-la

您可以压缩为:

Fa-la(3,9)

对于您来说,这似乎很奇怪,但是该方法有效:复制了前三个“ -la”的字节之后,将继续使用新生成的字节进行复制。

实际上,这是序列长度的一种编码,其中重复复制部分数据以获得所需的长度。

科林·莫里斯(Colin Morris)的一篇文章中展示了使用Lempel-Ziv压缩进行歌词的交互式示例,流行歌词是否变得更具重复性?

以下是在C中复制反向链接的示例。请注意,由于可能的重叠,我们不能使用memcpymemmove

/* Output the (dist,len) backref at dst_pos in dst. */
static inline void lz77_output_backref(uint8_t *dst, size_t dst_pos,
                                       size_t dist, size_t len)
{
        size_t i;

        assert(dist <= dst_pos && "cannot reference before beginning of dst");

        for (i = 0; i < len; i++) {
                dst[dst_pos] = dst[dst_pos - dist];
                dst_pos++;
        }
}

生成文字很容易,但是为了完整起见,我们将使用一个辅助函数:

/* Output lit at dst_pos in dst. */
static inline void lz77_output_lit(uint8_t *dst, size_t dst_pos, uint8_t lit)
{
        dst[dst_pos] = lit;
}

请注意,此函数的调用者必须确保有dst足够的空间用于生成的数据,并且在缓冲区开始之前,反向链接不会访问该位置。

很难在解压缩过程中不使用反向链接来生成数据,而要在压缩源数据时首先创建它。可以通过不同的方式完成此操作,但是我们将使用基于zlib哈希表的方法,该方法是RFC 1951中提出的。

我们将使用在表中以前找到的具有三个字符前缀位置的哈希表(较短的反向链接不会带来任何好处)。 Deflate允许在前32,768个字符内进行反向链接-这称为窗口。这提供了流式压缩:如果最后一个字节的窗口存储在内存中,则一次只对输入数据进行一点处理。但是,我们的实现假定所有输入数据对我们都是可用的,并且我们可以一次处理整个数据。这使您可以专注于压缩而不是计费,这对于流处理是必需的。

我们将使用两个数组:in head包含输入中位置的三个字符前缀的哈希值,in prev包含具有此哈希值的前一个位置的位置。实际上,head[h]这是带有哈希值的前缀位置的链接列表的标题h,并prev[x]接收x列表前面元素

#define LZ_WND_SIZE 32768
#define LZ_MAX_LEN  258

#define HASH_SIZE 15
#define NO_POS    SIZE_MAX

/* Perform LZ77 compression on the len bytes in src. Returns false as soon as
   either of the callback functions returns false, otherwise returns true when
   all bytes have been processed. */
bool lz77_compress(const uint8_t *src, size_t len,
                   bool (*lit_callback)(uint8_t lit, void *aux),
                   bool (*backref_callback)(size_t dist, size_t len, void *aux),
                   void *aux)
{
        size_t head[1U << HASH_SIZE];
        size_t prev[LZ_WND_SIZE];

        uint16_t h;
        size_t i, j, dist;
        size_t match_len, match_pos;
        size_t prev_match_len, prev_match_pos;

        /* Initialize the hash table. */
        for (i = 0; i < sizeof(head) / sizeof(head[0]); i++) {
                head[i] = NO_POS;
        }

要将新的字符串位置插入哈希表prev,将对其进行更新以指示先前的位置head,然后对其进行更新head

static void insert_hash(uint16_t hash, size_t pos, size_t *head, size_t *prev)
{
        prev[pos % LZ_WND_SIZE] = head[hash];
        head[hash] = pos;
}

索引时请注意模运算prev:我们仅对落入当前窗口的那些位置感兴趣。

与其从头开始为每个三个字符的前缀计算哈希值,我们将使用一个环形哈希并不断更新它,以便在其值中仅反映最后三个字符:

static uint16_t update_hash(uint16_t hash, uint8_t c)
{
        hash <<= 5;                     /* Shift out old bits. */
        hash ^= c;                      /* Include new bits. */
        hash &= (1U << HASH_SIZE) - 1;  /* Mask off excess bits. */

        return hash;
}

哈希图随后可用于有效地搜索具有序列的先前匹配,如下所示。搜索匹配项是最耗费资源的压缩操作,因此我们将限制列表中搜索的深度。

如下所述,更改各种参数(例如前缀列表中的搜索深度)并执行延迟比较,是通过降低压缩率来提高速度的一种方法。选择我们代码中的设置以匹配zlib中的最大压缩级别。

/* Find the longest most recent string which matches the string starting
 * at src[pos]. The match must be strictly longer than prev_match_len and
 * shorter or equal to max_match_len. Returns the length of the match if found
 * and stores the match position in *match_pos, otherwise returns zero. */
static size_t find_match(const uint8_t *src, size_t pos, uint16_t hash,
                         size_t prev_match_len, size_t max_match_len,
                         const size_t *head, const size_t *prev,
                         size_t *match_pos)
{
        size_t max_match_steps = 4096;
        size_t i, l;
        bool found;

        if (prev_match_len == 0) {
                /* We want backrefs of length 3 or longer. */
                prev_match_len = 2;
        }

        if (prev_match_len >= max_match_len) {
                /* A longer match would be too long. */
                return 0;
        }

        if (prev_match_len >= 32) {
                /* Do not try too hard if there is already a good match. */
                max_match_steps /= 4;
        }

        found = false;
        i = head[hash];

        while (max_match_steps != 0) {
                if (i == NO_POS) {
                        /* No match. */
                        break;
                }

                assert(i < pos && "Matches should precede pos.");
                if (pos - i > LZ_WND_SIZE) {
                        /* The match is outside the window. */
                        break;
                }

                l = cmp(src, i, pos, prev_match_len, max_match_len);

                if (l != 0) {
                        assert(l > prev_match_len);
                        assert(l <= max_match_len);

                        found = true;
                        *match_pos = i;
                        prev_match_len = l;

                        if (l == max_match_len) {
                                /* A longer match is not possible. */
                                return l;
                        }
                }

                /* Look further back in the prefix list. */
                i = prev[i % LZ_WND_SIZE];
                max_match_steps--;
        }

        if (!found) {
                return 0;
        }

        return prev_match_len;
}

/* Compare the substrings starting at src[i] and src[j], and return the length
 * of the common prefix. The match must be strictly longer than prev_match_len
 * and shorter or equal to max_match_len. */
static size_t cmp(const uint8_t *src, size_t i, size_t j,
                  size_t prev_match_len, size_t max_match_len)
{
        size_t l;

        assert(prev_match_len < max_match_len);

        /* Check whether the first prev_match_len + 1 characters match. Do this
         * backwards for a higher chance of finding a mismatch quickly. */
        for (l = 0; l < prev_match_len + 1; l++) {
                if (src[i + prev_match_len - l] !=
                    src[j + prev_match_len - l]) {
                        return 0;
                }
        }

        assert(l == prev_match_len + 1);

        /* Now check how long the full match is. */
        for (; l < max_match_len; l++) {
                if (src[i + l] != src[j + l]) {
                        break;
                }
        }

        assert(l > prev_match_len);
        assert(l <= max_match_len);
        assert(memcmp(&src[i], &src[j], l) == 0);

        return l;
}

您可以使用lz77_compress以下代码终止该函数以搜索先前的匹配项:

       /* h is the hash of the three-byte prefix starting at position i. */
        h = 0;
        if (len >= 2) {
                h = update_hash(h, src[0]);
                h = update_hash(h, src[1]);
        }

        prev_match_len = 0;
        prev_match_pos = 0;

        for (i = 0; i + 2 < len; i++) {
                h = update_hash(h, src[i + 2]);

                /* Search for a match using the hash table. */
                match_len = find_match(src, i, h, prev_match_len,
                                       min(LZ_MAX_LEN, len - i), head, prev,
                                       &match_pos);

                /* Insert the current hash for future searches. */
                insert_hash(h, i, head, prev);

                /* If the previous match is at least as good as the current. */
                if (prev_match_len != 0 && prev_match_len >= match_len) {
                        /* Output the previous match. */
                        dist = (i - 1) - prev_match_pos;
                        if (!backref_callback(dist, prev_match_len, aux)) {
                                return false;
                        }
                        /* Move past the match. */
                        for (j = i + 1; j < min((i - 1) + prev_match_len,
                                                len - 2); j++) {
                                h = update_hash(h, src[j + 2]);
                                insert_hash(h, j, head, prev);
                        }
                        i = (i - 1) + prev_match_len - 1;
                        prev_match_len = 0;
                        continue;
                }

                /* If no match (and no previous match), output literal. */
                if (match_len == 0) {
                        assert(prev_match_len == 0);
                        if (!lit_callback(src[i], aux)) {
                                return false;
                        }
                        continue;
                }

                /* Otherwise the current match is better than the previous. */

                if (prev_match_len != 0) {
                        /* Output a literal instead of the previous match. */
                        if (!lit_callback(src[i - 1], aux)) {
                                return false;
                        }
                }

                /* Defer this match and see if the next is even better. */
                prev_match_len = match_len;
                prev_match_pos = match_pos;
        }

        /* Output any previous match. */
        if (prev_match_len != 0) {
                dist = (i - 1) - prev_match_pos;
                if (!backref_callback(dist, prev_match_len, aux)) {
                        return false;
                }
                i = (i - 1) + prev_match_len;
        }

        /* Output any remaining literals. */
        for (; i < len; i++) {
                if (!lit_callback(src[i], aux)) {
                        return false;
                }
        }

        return true;
}

此代码正在寻找可以在当前位置生成的最长反向链接。但是在发布之前,程序会决定是否有可能在下一个位置找到更长的比赛。在zlib中,这称为惰性比较评估。这仍然是一个贪婪的算法:即使当前较短的匹配项允许您以后再获得更长的匹配项并实现更强的压缩,它也会选择最长的匹配项。

Lempel-Ziv压缩既可以快速又可以缓慢地工作。佐普利花了很多时间寻找最佳反向链接来压缩额外的压缩百分比。这对于一次压缩然后再使用的数据很有用,例如,对于Web服务器上的静态信息。另一方面,是诸如SnappyLZ4之类的压缩器,它们仅与最后4个字节的前缀进行比较,并且速度非常快。这种类型的压缩在数据库和RPC系统中非常有用,在这些数据库和RPC系统中,通过在网络上或磁盘上发送数据可以节省时间,从而节省了压缩时间。

将源数据用作字典的想法非常优雅,但是您也可以从静态字典中受益。Brotli是基于LZ77的算法,但它也使用了大型算法静态字符串字典,通常在网络上找到。

可以在lz77.hlz77.c中查看LZ77代码

霍夫曼密码


第二个Zip压缩算法是霍夫曼代码。在本文中

,术语“ 代码”是对用于以某种其他形式表示数据的系统的引用。在这种情况下,我们对可用于有效表示由Lempel-Ziv算法生成的文字和反向链接的代码感兴趣。

传统上,英文文本是使用美国信息交换标准代码(ASCII)表示的该系统为每个字符分配一个数字,该数字通常以8位表示形式存储。以下是英文字母大写字母的ASCII码:

一个01000001N01001110
B01000010O01001111
C01000011P01010000
D01000100Q01010001
E01000101R01010010
F01000110S01010011
G01000111T01010100
H01001000U01010101
I01001001V01010110
J01001010W01010111
K01001011X01011000
L01001100Y01011001
M01001101Z01011010

每个字符一个字节是一种方便的文本存储方式。它使您可以轻松地访问或修改文本的各个部分,并且始终可以清楚地知道存储N个字符需要多少个字节,或存储在N个字节中需要多少个字符。但是,就占用空间而言,这不是最有效的方法。例如,在英语中,字母E最常使用,而Z是最不常用。因此,就数量而言,对E使用较短的位表示,对Z使用较长的位表示,而不是为每个字符分配相同数量的位,会更有效率。

将不同长度的编码指定给不同源字符的代码称为可变长度代码。最著名的例子是莫尔斯电码。,其中每个字符都由点和破折号编码,最初是通过电报以短脉冲和长脉冲发送的:

一个•-ñ-•
-•••Ø---
C-•-•P•--•
d-••--•-
Ë[R•-•
F••-•小号•••
G--•Ť--
H••••ü••-
一世••V•••-
Ĵ•---w ^•--
ķ-•-X-••-
大号•-••ÿ-•--
中号--ž--••

摩尔斯电码的缺点之一是一个码字可能是另一个的前缀。例如,••-•没有唯一的解码:它可以是F或ER。这可以通过在传输过程中字母之间的停顿(长度为三个点)来解决。但是,如果代码字不能是其他单词的前缀,那就更好了。此代码称为unrefixed。固定长度的ASCII代码是不固定的,因为代码字总是相同的长度。但是可变长度代码也可以不固定。电话号码通常是不固定的。在瑞典引入紧急电话号码112之前,必须更改所有以112开头的电话号码,而在美国,没有一个以911开头的电话号码。

为了最小化编码消息的大小,最好使用不固定的代码,其中频繁出现的字符的代码字较短。最佳代码将是产生最短结果的代码-代码字长度的总和乘以它们的出现频率将是最小的。这被称为具有最小冗余非前缀代码霍夫曼代码,以纪念用于生成此类代码的高效算法的发明者。

霍夫曼算法


大卫·霍夫曼(David Huffman)在麻省理工学院(MIT)学习撰写电子工程博士学位论文的材料时,参加了罗伯特·法诺(Robert Fano)教授的信息论课程。根据传说,法诺允许他的学生选择:写期末考试或课程。霍夫曼选择了后者,他得到了以最小冗余度搜索无前缀代码的主题。假定他当时不知道Fano自己在从事这项工作(Shannon-Fano算法是当时最著名的方法)。霍夫曼的著作于1952年以1952年的“最小冗余码的构造方法”出版。此后,其算法得到了广泛的应用。


David Huffman 新闻稿 UC Santa Cruz。

霍夫曼算法创建的非固定代码在字符集及其使用频率上具有最小的冗余。该算法反复选择在源数据中最不可能找到的两个字符(例如X和Y),并用含义为“ X或Y” 复合字符替换它们复合符号的出现频率是两个源符号的频率之和。X和Y的代码字可以是分配给复合字符“ X或Y”后跟0或1以区分原始字符的任何代码字。当输入数据减少到一个字符时,该算法停止工作(视频说明)。

这是在小型字符集上工作的算法示例:

符号频率
一个6
4
C2
d3

第一次迭代处理:


从集合中删除两个最稀有的符号C和D,并用频率为C和D的总和的复合符号代替:


现在,最稀有的符号是B和频率为5的复合符号。将它们从集合中删除,并替换为频率为9的复合符号:


最后,将A和频率为9的复合符号组合为频率为15的新符号:


整个集合缩减为一个字符,处理完成。

该算法创建了一种称为霍夫曼树的结构输入字符是叶子,并且字符的频率越高,它位于的位置越高。从树的根部开始,您可以通过向左或向右移动分别添加0或1来生成字符的代码字。原来是这样的:

符号码字
一个0
10
C110
d111

任何代码字都不是其他任何字词的前缀。符号出现的频率越高,其代码字越短。

树也可以用于解码:我们从根开始,向右或向左查找字符前面带有0或1的值。例如,行010100在ABBA中解码。

注意,每个码字的长度等于相应树节点的深度。正如我们将在下一部分中看到的,我们不需要真正的树来分配代码字。知道单词本身的长度就足够了。因此,我们实现霍夫曼算法的结果将是代码字的长度。

为了存储字符集并有效地找到最低频率,我们将使用二进制堆数据结构。我们特别感兴趣min-heap,因为最小值应该在顶部。

/* Swap the 32-bit values pointed to by a and b. */
static void swap32(uint32_t *a, uint32_t *b)
{
        uint32_t tmp;

        tmp = *a;
        *a = *b;
        *b = tmp;
}

/* Move element i in the n-element heap down to restore the minheap property. */
static void minheap_down(uint32_t *heap, size_t n, size_t i)
{
        size_t left, right, min;

        assert(i >= 1 && i <= n && "i must be inside the heap");

        /* While the ith element has at least one child. */
        while (i * 2 <= n) {
                left = i * 2;
                right = i * 2 + 1;

                /* Find the child with lowest value. */
                min = left;
                if (right <= n && heap[right] < heap[left]) {
                        min = right;
                }

                /* Move i down if it is larger. */
                if (heap[min] < heap[i]) {
                        swap32(&heap[min], &heap[i]);
                        i = min;
                } else {
                        break;
                }
        }
}

/* Establish minheap property for heap[1..n]. */
static void minheap_heapify(uint32_t *heap, size_t n)
{
        size_t i;

        /* Floyd's algorithm. */
        for (i = n / 2; i >= 1; i--) {
                minheap_down(heap, n, i);
        }
}

为了跟踪n字符的频率,我们将使用一堆n元素。另外,每次创建复合符号时,我们都希望将两个源符号“链接”到该符号。因此,每个符号都会有一个“交流元素”。

为了存储n-element堆和n通讯元素,我们将使用n * 2 + 1元素数组当堆上的两个字符替换为一个字符时,我们将使用第二个元素将链接保存到新字符。该方法基于“ 管理 Witten,Moffat和Bell的千兆字节”的实现

在堆中的每个节点上,我们将使用16个最高有效位来存储符号频率,并使用16个最低有效位来存储符号通信元素的索引。由于使用高位,频率差将由堆的两个元素之间的32位比较结果确定。

由于这种表示,我们需要确保字符的频率始终适合16位。算法完成后,最终的复合符号将具有所有组合符号的频率,也就是说,该总和应放置在16位中。我们的Deflate实现将通过同时处理多达64,535个字符来验证这一点。

频率为零的符号将接收长度为零的代码字,并且不会参与编码的编译。

如果代码字达到指定的最大深度,我们将通过施加频率限制来“平滑”频率分布,然后重试(是,在帮助下goto)。进行深度限制霍夫曼编码的方法更加复杂,但是这种方法简单有效。

#define MAX_HUFFMAN_SYMBOLS 288      /* Deflate uses max 288 symbols. */

/* Construct a Huffman code for n symbols with the frequencies in freq, and
 * codeword length limited to max_len. The sum of the frequencies must be <=
 * UINT16_MAX. max_len must be large enough that a code is always possible,
 * i.e. 2 ** max_len >= n. Symbols with zero frequency are not part of the code
 * and get length zero. Outputs the codeword lengths in lengths[0..n-1]. */
static void compute_huffman_lengths(const uint16_t *freqs, size_t n,
                                    uint8_t max_len, uint8_t *lengths)
{
        uint32_t nodes[MAX_HUFFMAN_SYMBOLS * 2 + 1], p, q;
        uint16_t freq;
        size_t i, h, l;
        uint16_t freq_cap = UINT16_MAX;

#ifndef NDEBUG
        uint32_t freq_sum = 0;
        for (i = 0; i < n; i++) {
                freq_sum += freqs[i];
        }
        assert(freq_sum <= UINT16_MAX && "Frequency sum too large!");
#endif

        assert(n <= MAX_HUFFMAN_SYMBOLS);
        assert((1U << max_len) >= n && "max_len must be large enough");

try_again:
        /* Initialize the heap. h is the heap size. */
        h = 0;
        for (i = 0; i < n; i++) {
                freq = freqs[i];

                if (freq == 0) {
                        continue; /* Ignore zero-frequency symbols. */
                }
                if (freq > freq_cap) {
                        freq = freq_cap; /* Enforce the frequency cap. */
                }

                /* High 16 bits: Symbol frequency.
                   Low 16 bits:  Symbol link element index. */
                h++;
                nodes[h] = ((uint32_t)freq << 16) | (uint32_t)(n + h);
        }
        minheap_heapify(nodes, h);

        /* Special case for less than two non-zero symbols. */
        if (h < 2) {
                for (i = 0; i < n; i++) {
                        lengths[i] = (freqs[i] == 0) ? 0 : 1;
                }
                return;
        }

        /* Build the Huffman tree. */
        while (h > 1) {
                /* Remove the lowest frequency node p from the heap. */
                p = nodes[1];
                nodes[1] = nodes[h--];
                minheap_down(nodes, h, 1);

                /* Get q, the next lowest frequency node. */
                q = nodes[1];

                /* Replace q with a new symbol with the combined frequencies of
                   p and q, and with the no longer used h+1'th node as the
                   link element. */
                nodes[1] = ((p & 0xffff0000) + (q & 0xffff0000))
                           | (uint32_t)(h + 1);

                /* Set the links of p and q to point to the link element of
                   the new node. */
                nodes[p & 0xffff] = nodes[q & 0xffff] = (uint32_t)(h + 1);

                /* Move the new symbol down to restore heap property. */
                minheap_down(nodes, h, 1);
        }

        /* Compute the codeword length for each symbol. */
        h = 0;
        for (i = 0; i < n; i++) {
                if (freqs[i] == 0) {
                        lengths[i] = 0;
                        continue;
                }
                h++;

                /* Link element for the i'th symbol. */
                p = nodes[n + h];

                /* Follow the links until we hit the root (link index 2). */
                l = 1;
                while (p != 2) {
                        l++;
                        p = nodes[p];
                }

                if (l > max_len) {
                        /* Lower freq_cap to flatten the distribution. */
                        assert(freq_cap != 1 && "Cannot lower freq_cap!");
                        freq_cap /= 2;
                        goto try_again;
                }

                assert(l <= UINT8_MAX);
                lengths[i] = (uint8_t)l;
        }
}

二进制堆选项的一种很好的替代选择是将字符存储在两个队列中。第一个包含源字符,按频率排序。创建复合符号后,将其第二次添加。因此,具有最低频率的符号将总是在一个队列中的第一位置。扬·范·利文Jan van Leeuwen)《霍夫曼树的构造》(1976)中对此方法进行了描述

霍夫曼编码最适合非前缀编码,但是在其他情况下,还有更有效的方法:算术编码非对称数字系统

霍夫曼规范代码


在上面的示例中,我们构建了一个霍夫曼树:


如果我们从根开始,将0用作左侧分支,将1用作右侧,则将获得以下代码:

符号码字
一个0
10
C110
d111

对于左分支使用0,对右分支使用1的决定似乎是任意的。如果我们做相反的事情,我们得到:

符号码字
一个1个
01
C001
d000

我们可以任意地将一个节点的两个分支标记为零和一(主要是标签是不同的),并且仍然得到等效的代码:


符号码字
一个0
十一
C100
d101

尽管霍夫曼算法以最小的冗余度为未固定的代码提供了所需的代码字长度,但仍有许多方法可以分配单个代码字。

给定通过霍夫曼算法计算出的代码字的长度,规范霍夫曼代码以特定方式将代码字分配给字符。这很有用,因为它允许您存储和传输带有压缩数据的码字长度:解码器将能够根据其长度恢复码字。当然,您可以存储和传输符号频率,并在解码器中运行霍夫曼算法,但这将需要更多的工作和解码器的更多存储。另一个非常重要的特性是规范代码的结构使用了有效的解码。

想法是一次将代码字依次分配给字符。第一个代码字为0。下一个代码字为前一个字+ 1的长度。第一个长度为N的字由长度为N-1的最后一个字组成,加一个(以获得一个新的代码字),并向左移动一个步长(以增加长度)。

在霍夫曼树的术语中,代码字按从左到右的顺序依次分配给叶子,一次一个级别,当移到下一级别时向左移动。

在我们的ABCD示例中,霍夫曼算法分配的码字的长度分别为1、2、3和3。第一个字为0。这也是长度1的最后一个字。对于长度2,我们取0并加1得到下一个代码,它将成为两位代码的前缀。 ,向左移动并得到10。这是长度2的最后一个单词。要获得长度3,我们加1并移位:110。要得到长度3的下一个单词,我们加1:111。

符号码字
一个0
10
C110
d111

规范代码生成器的实现如下所示。注意,Deflate算法期望根据LSB优先原则(第一位,最低有效位)生成码字。即,码字的第一位应存储在最低有效位中。这意味着我们需要更改位的顺序,例如,使用查找表。

#define MAX_HUFFMAN_BITS 15          /* Deflate uses max 15-bit codewords. */

static void compute_canonical_code(uint16_t *codewords, const uint8_t *lengths,
                                   size_t n)
{
        size_t i;
        uint16_t count[MAX_HUFFMAN_BITS + 1] = {0};
        uint16_t code[MAX_HUFFMAN_BITS + 1];
        int l;

        /* Count the number of codewords of each length. */
        for (i = 0; i < n; i++) {
                count[lengths[i]]++;
        }
        count[0] = 0; /* Ignore zero-length codes. */

        /* Compute the first codeword for each length. */
        code[0] = 0;
        for (l = 1; l <= MAX_HUFFMAN_BITS; l++) {
                code[l] = (uint16_t)((code[l - 1] + count[l - 1]) << 1);
        }

        /* Assign a codeword for each symbol. */
        for (i = 0; i < n; i++) {
                l = lengths[i];
                if (l == 0) {
                        continue;
                }

                codewords[i] = reverse16(code[l]++, l); /* Make it LSB-first. */
        }
}

/* Reverse the n least significant bits of x.
   The (16 - n) most significant bits of the result will be zero. */
static inline uint16_t reverse16(uint16_t x, int n)
{
        uint16_t lo, hi;
        uint16_t reversed;

        assert(n > 0);
        assert(n <= 16);

        lo = x & 0xff;
        hi = x >> 8;

        reversed = (uint16_t)((reverse8_tbl[lo] << 8) | reverse8_tbl[hi]);

        return reversed >> (16 - n);
}

现在,将它们放在一起并编写编码器初始化代码:

typedef struct huffman_encoder_t huffman_encoder_t;
struct huffman_encoder_t {
        uint16_t codewords[MAX_HUFFMAN_SYMBOLS]; /* LSB-first codewords. */
        uint8_t lengths[MAX_HUFFMAN_SYMBOLS];    /* Codeword lengths. */
};

/* Initialize a Huffman encoder based on the n symbol frequencies. */
void huffman_encoder_init(huffman_encoder_t *e, const uint16_t *freqs, size_t n,
                          uint8_t max_codeword_len)
{
        assert(n <= MAX_HUFFMAN_SYMBOLS);
        assert(max_codeword_len <= MAX_HUFFMAN_BITS);

        compute_huffman_lengths(freqs, n, max_codeword_len, e->lengths);
        compute_canonical_code(e->codewords, e->lengths, n);
}

我们还提供了使用已经计算出的代码长度来配置编码器的功能:

/* Initialize a Huffman encoder based on the n codeword lengths. */
void huffman_encoder_init2(huffman_encoder_t *e, const uint8_t *lengths,
                           size_t n)
{
        size_t i;

        for (i = 0; i < n; i++) {
                e->lengths[i] = lengths[i];
        }
        compute_canonical_code(e->codewords, e->lengths, n);
}

高效霍夫曼解码


解码霍夫曼的最简单方法是从根开始遍历树,一次读取一位输入,并确定下一步,向左或向右进行分支。当到达叶节点时,它是一个解码字符。

这种方法通常在大学和书籍中讲授。它简单而优雅,但是一次处理一次太慢。使用查找表进行解码要快得多。对于上面的示例,其中最大码字长度为三位,可以使用下表:

符号码字长度
0 00一个1个
0 01一个1个
0 10一个1个
0 11一个1个
10 02
10 12
110C3
111d3

尽管只有四个字符,但我们需要一个包含八个条目的表,以涵盖所有可能的三位组合。码字少于三位的符号在表中有多个条目。例如,单词10被10 0和10 1 “补充” 以覆盖以10开头的所有三位组合。

要以这种方式解码,您需要在表中用以下三个输入位索引,并立即找到相应的字符及其代码字的长度。长度很重要,因为尽管看了下三个比特,我们仍需要获得与代码字长度相同数量的输入比特。

基于搜索表的方法非常快速,但是它有一个缺点:表的大小会随着代码字长度的每增加一位而加倍。也就是说,表的构造成倍地减慢了速度,如果它不再适合处理器高速缓存,那么该方法将开始缓慢运行。

因此,查找表通常仅用于不大于特定长度的代码字。对于更长的单词,请采用其他方法。就像霍夫曼编码将较短的代码字分配给更频繁的字符一样,在许多情况下,对较短的代码字使用查找表是一种很好的优化方法。

在zlib中使用了几级搜索表。如果代码字对于第一个表来说太长,那么搜索将转到第二个表以索引其余位。

但是还有另一种非常优雅的方法,它基于规范的霍夫曼编码的性质。在《最小冗余前缀代码的实现》(Moffat和Turpin,1997)中有所描述,并在Charles Bloom的《失落的霍夫曼论文》中进行了解释

让我们从规范的版本中提取代码字:0、10、110、111。我们将跟踪每个长度的第一个代码字,以及每个序列中的每个代码字的编号-“符号索引”。

码字长度第一个码字第一个字符索引
1个01(A)
2102(B)
31103(C)

由于代码字是按顺序分配的,因此,如果我们知道位数,就可以在上表中找到这些位数代表的符号。例如,对于三位111,我们看到这是与该长度的第一个码字(110)的偏移量。该长度的第一个字符索引为3,偏移量为1则给我们的索引为4。另一个表将字符索引与字符进行比较:

sym_idx = d->first_symbol[len] + (bits - d->first_code[len]);
sym = d->syms[sym_idx];

进行一些优化:我们可以将第一个索引减去第一个代码字存储在表中,而不是分别存储第一个字符索引和第一个代码字:

sym_idx = d->offset_first_sym_idx[len] + bits;
sym = d->syms[sym_idx];

为了了解需要估计多少位,我们再次使用代码序列属性。在我们的示例中,所有有效的一位代码字均严格小于1,两位-严格低于11,三位-小于1000(实际上,所有三位值都为true)。换句话说,有效的N位代码字必须严格小于第一个N位代码字加上N位代码字的数量。此外,我们可以将这些边界向左移动,以便它们都是三位宽。我们称其为每个码字长度的限制性位

码字长度限制位
1个100
2110
31000

长度为3的限制器溢出到4位,但这仅意味着任何三位字都可以。

我们可以在三位输入数据中搜索并与限制性位进行比较,以了解我们的代码字有多长。完成后,我们移位输入位,仅计算它们的正确数字,然后找到字符索引:

for (len = 1; len <= 3; len++) {
        if (bits < d->sentinel_bits[len]) {
                bits >>= 3 - len;  /* Get the len most significant bits. */
                sym_idx = d->offset_first_sym_idx[len] + bits;
        }
}

该过程的时间复杂度相对于代码字中的位数是线性的,但是有效地利用了位置,每个步骤仅需要加载和比较,并且由于较短的代码字更为常见,因此该方法可以在许多情况下优化压缩。

完整的解码器代码:

#define HUFFMAN_LOOKUP_TABLE_BITS 8  /* Seems a good trade-off. */

typedef struct huffman_decoder_t huffman_decoder_t;
struct huffman_decoder_t {
        /* Lookup table for fast decoding of short codewords. */
        struct {
                uint16_t sym : 9;  /* Wide enough to fit the max symbol nbr. */
                uint16_t len : 7;  /* 0 means no symbol. */
        } table[1U << HUFFMAN_LOOKUP_TABLE_BITS];

        /* "Sentinel bits" value for each codeword length. */
        uint16_t sentinel_bits[MAX_HUFFMAN_BITS + 1];

        /* First symbol index minus first codeword mod 2**16 for each length. */
        uint16_t offset_first_sym_idx[MAX_HUFFMAN_BITS + 1];

        /* Map from symbol index to symbol. */
        uint16_t syms[MAX_HUFFMAN_SYMBOLS];
#ifndef NDEBUG
        size_t num_syms;
#endif
};

/* Get the n least significant bits of x. */
static inline uint64_t lsb(uint64_t x, int n)
{
        assert(n >= 0 && n <= 63);
        return x & (((uint64_t)1 << n) - 1);
}

/* Use the decoder d to decode a symbol from the LSB-first zero-padded bits.
 * Returns the decoded symbol number or -1 if no symbol could be decoded.
 * *num_used_bits will be set to the number of bits used to decode the symbol,
 * or zero if no symbol could be decoded. */
static inline int huffman_decode(const huffman_decoder_t *d, uint16_t bits,
                                 size_t *num_used_bits)
{
        uint64_t lookup_bits;
        size_t l;
        size_t sym_idx;

        /* First try the lookup table. */
        lookup_bits = lsb(bits, HUFFMAN_LOOKUP_TABLE_BITS);
        assert(lookup_bits < sizeof(d->table) / sizeof(d->table[0]));
        if (d->table[lookup_bits].len != 0) {
                assert(d->table[lookup_bits].len <= HUFFMAN_LOOKUP_TABLE_BITS);
                assert(d->table[lookup_bits].sym < d->num_syms);

                *num_used_bits = d->table[lookup_bits].len;
                return d->table[lookup_bits].sym;
        }

        /* Then do canonical decoding with the bits in MSB-first order. */
        bits = reverse16(bits, MAX_HUFFMAN_BITS);
        for (l = HUFFMAN_LOOKUP_TABLE_BITS + 1; l <= MAX_HUFFMAN_BITS; l++) {
                if (bits < d->sentinel_bits[l]) {
                        bits >>= MAX_HUFFMAN_BITS - l;

                        sym_idx = (uint16_t)(d->offset_first_sym_idx[l] + bits);
                        assert(sym_idx < d->num_syms);

                        *num_used_bits = l;
                        return d->syms[sym_idx];
                }
        }

        *num_used_bits = 0;
        return -1;
}

要配置解码器,我们将像huffman_encoder_init一样预先计算规范代码,并填写不同的表:

/* Initialize huffman decoder d for a code defined by the n codeword lengths.
   Returns false if the codeword lengths do not correspond to a valid prefix
   code. */
bool huffman_decoder_init(huffman_decoder_t *d, const uint8_t *lengths,
                          size_t n)
{
        size_t i;
        uint16_t count[MAX_HUFFMAN_BITS + 1] = {0};
        uint16_t code[MAX_HUFFMAN_BITS + 1];
        uint32_t s;
        uint16_t sym_idx[MAX_HUFFMAN_BITS + 1];
        int l;

#ifndef NDEBUG
        assert(n <= MAX_HUFFMAN_SYMBOLS);
        d->num_syms = n;
#endif

        /* Zero-initialize the lookup table. */
        for (i = 0; i < sizeof(d->table) / sizeof(d->table[0]); i++) {
                d->table[i].len = 0;
        }

        /* Count the number of codewords of each length. */
        for (i = 0; i < n; i++) {
                assert(lengths[i] <= MAX_HUFFMAN_BITS);
                count[lengths[i]]++;
        }
        count[0] = 0;  /* Ignore zero-length codewords. */

        /* Compute sentinel_bits and offset_first_sym_idx for each length. */
        code[0] = 0;
        sym_idx[0] = 0;
        for (l = 1; l <= MAX_HUFFMAN_BITS; l++) {
                /* First canonical codeword of this length. */
                code[l] = (uint16_t)((code[l - 1] + count[l - 1]) << 1);

                if (count[l] != 0 && code[l] + count[l] - 1 > (1U << l) - 1) {
                        /* The last codeword is longer than l bits. */
                        return false;
                }

                s = (uint32_t)((code[l] + count[l]) << (MAX_HUFFMAN_BITS - l));
                d->sentinel_bits[l] = (uint16_t)s;
                assert(d->sentinel_bits[l] == s && "No overflow.");

                sym_idx[l] = sym_idx[l - 1] + count[l - 1];
                d->offset_first_sym_idx[l] = sym_idx[l] - code[l];
        }

        /* Build mapping from index to symbol and populate the lookup table. */
        for (i = 0; i < n; i++) {
                l = lengths[i];
                if (l == 0) {
                        continue;
                }

                d->syms[sym_idx[l]] = (uint16_t)i;
                sym_idx[l]++;

                if (l <= HUFFMAN_LOOKUP_TABLE_BITS) {
                        table_insert(d, i, l, code[l]);
                        code[l]++;
                }
        }

        return true;
}

static void table_insert(huffman_decoder_t *d, size_t sym, int len,
                         uint16_t codeword)
{
        int pad_len;
        uint16_t padding, index;

        assert(len <= HUFFMAN_LOOKUP_TABLE_BITS);

        codeword = reverse16(codeword, len); /* Make it LSB-first. */
        pad_len = HUFFMAN_LOOKUP_TABLE_BITS - len;

        /* Pad the pad_len upper bits with all bit combinations. */
        for (padding = 0; padding < (1U << pad_len); padding++) {
                index = (uint16_t)(codeword | (padding << len));
                d->table[index].sym = (uint16_t)sym;
                d->table[index].len = (uint16_t)len;

                assert(d->table[index].sym == sym && "Fits in bitfield.");
                assert(d->table[index].len == len && "Fits in bitfield.");
        }
}

放气


Deflate算法是1993年PKZip 2.04c中引入的,是现代Zip文件中的一种标准压缩方法。它还以gzip,PNG和许多其他格式使用。它结合了LZ77压缩和霍夫曼编码,我们将在本节中进行讨论和实现。

在放气之前,PKZip使用了收缩,缩小和放大压缩方法。今天,它们很罕见,尽管在Deflate仍使用了一段时间之后,因为它们消耗的内存更少。但是我们不会考虑它们。

位流


Deflate根据LSB优先原理将Huffman码字存储在比特流中。这意味着流的第一位存储在第一字节的最低有效位中。

考虑一个比特流(从左到右读取)1-0-0-1-1。根据LSB优先原理进行存储时,字节值变为0b00011001(二进制)或0x19(十六进制)。似乎流只是简单地向后表示(从某种意义上说是这样),但是优点是我们更容易从计算机字中获取前N位:我们仅隐藏了N位最低有效位。

这些过程取自bitstream.h

/* Input bitstream. */
typedef struct istream_t istream_t;
struct istream_t {
        const uint8_t *src;  /* Source bytes. */
        const uint8_t *end;  /* Past-the-end byte of src. */
        size_t bitpos;       /* Position of the next bit to read. */
        size_t bitpos_end;   /* Position of past-the-end bit. */
};

/* Initialize an input stream to present the n bytes from src as an LSB-first
 * bitstream. */
static inline void istream_init(istream_t *is, const uint8_t *src, size_t n)
{
        is->src = src;
        is->end = src + n;
        is->bitpos = 0;
        is->bitpos_end = n * 8;
}

我们的霍夫曼解码器需要查看流中的以下位(足够长的码字,可能的最长码字),然后以解码符号所使用的位数继续流:

#define ISTREAM_MIN_BITS (64 - 7)

/* Get the next bits from the input stream. The number of bits returned is
 * between ISTREAM_MIN_BITS and 64, depending on the position in the stream, or
 * fewer if the end of stream is reached. The upper bits are zero-padded. */
static inline uint64_t istream_bits(const istream_t *is)
{
        const uint8_t *next;
        uint64_t bits;
        int i;

        next = is->src + (is->bitpos / 8);

        assert(next <= is->end && "Cannot read past end of stream.");

        if (is->end - next >= 8) {
                /* Common case: read 8 bytes in one go. */
                bits = read64le(next);
        } else {
                /* Read the available bytes and zero-pad. */
                bits = 0;
                for (i = 0; i < is->end - next; i++) {
                        bits |= (uint64_t)next[i] << (i * 8);
                }
        }

        return bits >> (is->bitpos % 8);
}

/* Advance n bits in the bitstream if possible. Returns false if that many bits
 * are not available in the stream. */
static inline bool istream_advance(istream_t *is, size_t n) {
        if (is->bitpos + n > is->bitpos_end) {
                return false;
        }

        is->bitpos += n;
        return true;
}

底线是在64位计算机上,istream_bits只要结构元素istream_t在寄存器中,通常就可以作为单引导指令和一些算术执行read64le在实施bits.h(现代编译器使用小端的原则将其转换为一个单一的64位下载):

/* Read a 64-bit value from p in little-endian byte order. */
static inline uint64_t read64le(const uint8_t *p)
{
        /* The one true way, see
         * https://commandcenter.blogspot.com/2012/04/byte-order-fallacy.html */
        return ((uint64_t)p[0] << 0)  |
               ((uint64_t)p[1] << 8)  |
               ((uint64_t)p[2] << 16) |
               ((uint64_t)p[3] << 24) |
               ((uint64_t)p[4] << 32) |
               ((uint64_t)p[5] << 40) |
               ((uint64_t)p[6] << 48) |
               ((uint64_t)p[7] << 56);
}

我们还需要一个函数来将比特流继续到下一个字节的边界:

/* Round x up to the next multiple of m, which must be a power of 2. */
static inline size_t round_up(size_t x, size_t m)
{
        assert((m & (m - 1)) == 0 && "m must be a power of two");
        return (x + m - 1) & (size_t)(-m); /* Hacker's Delight (2nd), 3-1. */
}

/* Align the input stream to the next 8-bit boundary and return a pointer to
 * that byte, which may be the past-the-end-of-stream byte. */
static inline const uint8_t *istream_byte_align(istream_t *is)
{
        const uint8_t *byte;

        assert(is->bitpos <= is->bitpos_end && "Not past end of stream.");

        is->bitpos = round_up(is->bitpos, 8);
        byte = is->src + is->bitpos / 8;
        assert(byte <= is->end);

        return byte;
}

对于传出的比特流,我们使用读取-修改-写入过程写入比特。快速情况下,您可以使用64位读取,某种位操作和64位写入来写入位。

/* Output bitstream. */
typedef struct ostream_t ostream_t;
struct ostream_t {
        uint8_t *dst;
        uint8_t *end;
        size_t bitpos;
        size_t bitpos_end;
};

/* Initialize an output stream to write LSB-first bits into dst[0..n-1]. */
static inline void ostream_init(ostream_t *os, uint8_t *dst, size_t n)
{
        os->dst = dst;
        os->end = dst + n;
        os->bitpos = 0;
        os->bitpos_end = n * 8;
}

/* Get the current bit position in the stream. */
static inline size_t ostream_bit_pos(const ostream_t *os)
{
        return os->bitpos;
}

/* Return the number of bytes written to the output buffer. */
static inline size_t ostream_bytes_written(ostream_t *os)
{
        return round_up(os->bitpos, 8) / 8;
}

/* Write n bits to the output stream. Returns false if there is not enough room
 * at the destination. */
static inline bool ostream_write(ostream_t *os, uint64_t bits, size_t n)
{
        uint8_t *p;
        uint64_t x;
        int shift, i;

        assert(n <= 57);
        assert(bits <= ((uint64_t)1 << n) - 1 && "Must fit in n bits.");

        if (os->bitpos_end - os->bitpos < n) {
                /* Not enough room. */
                return false;
        }

        p = &os->dst[os->bitpos / 8];
        shift = os->bitpos % 8;

        if (os->end - p >= 8) {
                /* Common case: read and write 8 bytes in one go. */
                x = read64le(p);
                x = lsb(x, shift);
                x |= bits << shift;
                write64le(p, x);
        } else {
                /* Slow case: read/write as many bytes as are available. */
                x = 0;
                for (i = 0; i < os->end - p; i++) {
                        x |= (uint64_t)p[i] << (i * 8);
                }
                x = lsb(x, shift);
                x |= bits << shift;
                for (i = 0; i < os->end - p; i++) {
                        p[i] = (uint8_t)(x >> (i * 8));
                }
        }

        os->bitpos += n;

        return true;
}

/* Write a 64-bit value x to dst in little-endian byte order. */
static inline void write64le(uint8_t *dst, uint64_t x)
{
        dst[0] = (uint8_t)(x >> 0);
        dst[1] = (uint8_t)(x >> 8);
        dst[2] = (uint8_t)(x >> 16);
        dst[3] = (uint8_t)(x >> 24);
        dst[4] = (uint8_t)(x >> 32);
        dst[5] = (uint8_t)(x >> 40);
        dst[6] = (uint8_t)(x >> 48);
        dst[7] = (uint8_t)(x >> 56);
}

我们还需要有效地将字节写入流。当然,您可以重复执行8位记录,但是使用起来会更快memcpy

/* Align the bitstream to the next byte boundary, then write the n bytes from
   src to it. Returns false if there is not enough room in the stream. */
static inline bool ostream_write_bytes_aligned(ostream_t *os,
                                               const uint8_t *src,
                                               size_t n)
{
        if (os->bitpos_end - round_up(os->bitpos, 8) < n * 8) {
                return false;
        }

        os->bitpos = round_up(os->bitpos, 8);
        memcpy(&os->dst[os->bitpos / 8], src, n);
        os->bitpos += n * 8;

        return true;
}

开箱(通货膨胀)


由于压缩算法称为减缩 -吹,从提取的东西空气-拆包过程有时称为充气如果您首先研究此过程,我们将了解格式的工作原理。您可以在deflate.hdeflate.c的第一部分bits.htables.htables.c(使用generate_tables.c生成的第一部分中查看代码

使用Deflate压缩的数据存储为一系列块。每个块均以3位标题开头,如果这是该系列的最后一个块,则其中的第一个(最低有效位)位置1,其他两位指示其类型。


共有三种类型的块:未压缩(0),使用固定霍夫曼代码(1)压缩和使用“动态”霍夫曼代码(2)压缩。

这段代码使用辅助功能对不同类型的块执行解压缩,我们将在以后实现:

typedef enum {
        HWINF_OK,   /* Inflation was successful. */
        HWINF_FULL, /* Not enough room in the output buffer. */
        HWINF_ERR   /* Error in the input data. */
} inf_stat_t;

/* Decompress (inflate) the Deflate stream in src. The number of input bytes
   used, at most src_len, is stored in *src_used on success. Output is written
   to dst. The number of bytes written, at most dst_cap, is stored in *dst_used
   on success. src[0..src_len-1] and dst[0..dst_cap-1] must not overlap.
   Returns a status value as defined above. */
inf_stat_t hwinflate(const uint8_t *src, size_t src_len, size_t *src_used,
                     uint8_t *dst, size_t dst_cap, size_t *dst_used)
{
        istream_t is;
        size_t dst_pos;
        uint64_t bits;
        bool bfinal;
        inf_stat_t s;

        istream_init(&is, src, src_len);
        dst_pos = 0;

        do {
                /* Read the 3-bit block header. */
                bits = istream_bits(&is);
                if (!istream_advance(&is, 3)) {
                        return HWINF_ERR;
                }
                bfinal = bits & 1;
                bits >>= 1;

                switch (lsb(bits, 2)) {
                case 0: /* 00: No compression. */
                        s = inf_noncomp_block(&is, dst, dst_cap, &dst_pos);
                        break;
                case 1: /* 01: Compressed with fixed Huffman codes. */
                        s = inf_fixed_block(&is, dst, dst_cap, &dst_pos);
                        break;
                case 2: /* 10: Compressed with "dynamic" Huffman codes. */
                        s = inf_dyn_block(&is, dst, dst_cap, &dst_pos);
                        break;
                default: /* Invalid block type. */
                        return HWINF_ERR;
                }

                if (s != HWINF_OK) {
                        return s;
                }
        } while (!bfinal);

        *src_used = (size_t)(istream_byte_align(&is) - src);

        assert(dst_pos <= dst_cap);
        *dst_used = dst_pos;

        return HWINF_OK;
}

未压缩的放气块


这些是“存储”块,最简单的类型。它从位流的下一个8位边界开始,带有16位字(len)表示块的长度。它的后面是另一个16位字(nlen),它补充了len个字(位的顺序被反转)。假定nlen充当简单的len校验和:如果文件损坏,则值可能不会互补,程序将能够检测到错误。


len和nlen之后是未压缩的数据。由于块长度是16位值,因此数据大小限制为65,535个字节。

static inf_stat_t inf_noncomp_block(istream_t *is, uint8_t *dst,
                                    size_t dst_cap, size_t *dst_pos)
{
        const uint8_t *p;
        uint16_t len, nlen;

        p = istream_byte_align(is);

        /* Read len and nlen (2 x 16 bits). */
        if (!istream_advance(is, 32)) {
                return HWINF_ERR; /* Not enough input. */
        }
        len  = read16le(p);
        nlen = read16le(p + 2);
        p += 4;

        if (nlen != (uint16_t)~len) {
                return HWINF_ERR;
        }

        if (!istream_advance(is, len * 8)) {
                return HWINF_ERR; /* Not enough input. */
        }

        if (dst_cap - *dst_pos < len) {
                return HWINF_FULL; /* Not enough room to output. */
        }

        memcpy(&dst[*dst_pos], p, len);
        *dst_pos += len;

        return HWINF_OK;
}

使用固定的霍夫曼代码放气


压缩的Deflate块使用霍夫曼代码表示LZ77文字序列。反向链接使用块结束标记断开。对于文字,反向链接长度和标记,使用小写的霍夫曼代码对于反向链接距离,使用dist代码


Litlen编码0-285范围内的值。值0-255用于文本字节,256是块结束标记,257-285用于反向链接长度。

反向链接的长度为3-258字节。根据下表,Litlen值确定从流中添加零个或多个其他位以获得完整长度的基本长度。例如,小数值269表示基本长度为19和两个额外的位。流中的两位相加得出的最终长度为19到22。

利特伦额外的位衣长
25703
25804
25905
26006
26107
26208
26309
264010
2651个11-12
2661个13-14岁
2671个15-16
2681个17-18
269219-22
270223–26
271227-30
272231–34
273335–42
274343-50
275351–58
276359–66
277467–82
278483–98
279499–114
2804115–130
2815131–162
2825163–194
2835195–226
2845227–257
2850258

请注意,一个litlen值284加5个额外的位可以表示从227到258的长度,但是,规范指出长度258(反向链接的最大长度)必须使用单独的litlen值来表示。在经常遇到最大长度的情况下,这可以减少编码。

解压缩器使用该表从基准值(减去257)获得基本长度和其他位:

/* Table of litlen symbol values minus 257 with corresponding base length
   and number of extra bits. */
struct litlen_tbl_t {
        uint16_t base_len : 9;
        uint16_t ebits : 7;
};
const struct litlen_tbl_t litlen_tbl[29] = {
/* 257 */ { 3, 0 },
/* 258 */ { 4, 0 },

...

/* 284 */ { 227, 5 },
/* 285 */ { 258, 0 }
};

Huffman的固定litlen代码是规范的,并使用以下代码字长度(286–287不是有效的litlen值,但它们涉及代码生成):

贬值码字长度
0–1438
144–2559
256–2797
280–2878

解压缩器将这些长度存储在一个表中,以方便传输至huffman_decoder_init

const uint8_t fixed_litlen_lengths[288] = {
/*   0 */ 8,
/*   1 */ 8,

...

/* 287 */ 8,
};

反向链接的距离从1到32,768不等,它们使用类似于长度编码方案的方案进行编码。霍夫曼代码dist编码从0到29的值,每个值都对应于基本长度,向其添加附加位以获得最终距离:

距离额外的位距离
001个
1个02
203
304
41个5-6
51个7-8
629-12
7213–16
8317-24
9325–32
10433–48
十一449–64
12565–96
十三597–128
146129–192
十五6193–256
十六7257–384
177385-512
十八8513-768
十九8769-1024
二十91025-1536
2191537–2048
22102049-3072
23103073–4096
24十一4097-6144
25十一6145–8192
26128193–12288
271212289–16384
28十三16385–24576
29日十三24577–32768

霍夫曼的固定代码dist是规范的。所有码字均为5位长。很简单,解压缩器将代码存储在可以使用的表中huffman_decoder_init(dist值30-31不正确。表明它们参与了霍夫曼代码的生成,但实际上没有作用):

const uint8_t fixed_dist_lengths[32] = {
/*  0 */ 5,
/*  1 */ 5,

...

/* 31 */ 5,
};

解压缩或解压缩代码-使用固定的霍夫曼代码对块进行放气:

static inf_stat_t inf_fixed_block(istream_t *is, uint8_t *dst,
                                  size_t dst_cap, size_t *dst_pos)
{
        huffman_decoder_t litlen_dec, dist_dec;

        huffman_decoder_init(&litlen_dec, fixed_litlen_lengths,
                             sizeof(fixed_litlen_lengths) /
                             sizeof(fixed_litlen_lengths[0]));
        huffman_decoder_init(&dist_dec, fixed_dist_lengths,
                             sizeof(fixed_dist_lengths) /
                             sizeof(fixed_dist_lengths[0]));

        return inf_block(is, dst, dst_cap, dst_pos, &litlen_dec, &dist_dec);
}

#define LITLEN_EOB 256
#define LITLEN_MAX 285
#define LITLEN_TBL_OFFSET 257
#define MIN_LEN 3
#define MAX_LEN 258

#define DISTSYM_MAX 29
#define MIN_DISTANCE 1
#define MAX_DISTANCE 32768

static inf_stat_t inf_block(istream_t *is, uint8_t *dst, size_t dst_cap,
                            size_t *dst_pos,
                            const huffman_decoder_t *litlen_dec,
                            const huffman_decoder_t *dist_dec)
{
        uint64_t bits;
        size_t used, used_tot, dist, len;
        int litlen, distsym;
        uint16_t ebits;

        while (true) {
                /* Read a litlen symbol. */
                bits = istream_bits(is);
                litlen = huffman_decode(litlen_dec, (uint16_t)bits, &used);
                bits >>= used;
                used_tot = used;

                if (litlen < 0 || litlen > LITLEN_MAX) {
                        /* Failed to decode, or invalid symbol. */
                        return HWINF_ERR;
                } else if (litlen <= UINT8_MAX) {
                        /* Literal. */
                        if (!istream_advance(is, used_tot)) {
                                return HWINF_ERR;
                        }
                        if (*dst_pos == dst_cap) {
                                return HWINF_FULL;
                        }
                        lz77_output_lit(dst, (*dst_pos)++, (uint8_t)litlen);
                        continue;
                } else if (litlen == LITLEN_EOB) {
                        /* End of block. */
                        if (!istream_advance(is, used_tot)) {
                                return HWINF_ERR;
                        }
                        return HWINF_OK;
                }

                /* It is a back reference. Figure out the length. */
                assert(litlen >= LITLEN_TBL_OFFSET && litlen <= LITLEN_MAX);
                len   = litlen_tbl[litlen - LITLEN_TBL_OFFSET].base_len;
                ebits = litlen_tbl[litlen - LITLEN_TBL_OFFSET].ebits;
                if (ebits != 0) {
                        len += lsb(bits, ebits);
                        bits >>= ebits;
                        used_tot += ebits;
                }
                assert(len >= MIN_LEN && len <= MAX_LEN);

                /* Get the distance. */
                distsym = huffman_decode(dist_dec, (uint16_t)bits, &used);
                bits >>= used;
                used_tot += used;

                if (distsym < 0 || distsym > DISTSYM_MAX) {
                        /* Failed to decode, or invalid symbol. */
                        return HWINF_ERR;
                }
                dist  = dist_tbl[distsym].base_dist;
                ebits = dist_tbl[distsym].ebits;
                if (ebits != 0) {
                        dist += lsb(bits, ebits);
                        bits >>= ebits;
                        used_tot += ebits;
                }
                assert(dist >= MIN_DISTANCE && dist <= MAX_DISTANCE);

                assert(used_tot <= ISTREAM_MIN_BITS);
                if (!istream_advance(is, used_tot)) {
                        return HWINF_ERR;
                }

                /* Bounds check and output the backref. */
                if (dist > *dst_pos) {
                        return HWINF_ERR;
                }
                if (round_up(len, 8) <= dst_cap - *dst_pos) {
                        output_backref64(dst, *dst_pos, dist, len);
                } else if (len <= dst_cap - *dst_pos) {
                        lz77_output_backref(dst, *dst_pos, dist, len);
                } else {
                        return HWINF_FULL;
                }
                (*dst_pos) += len;
        }
}

请注意此优化:当传出缓冲区中没有足够的空间时,我们使用下面的函数发出反向链接,该链接一次复制64位。从某种意义上说,这是“混乱的”,因为它通常会复制一些额外的字节(最多8个下一个倍数)。但是它工作得更快lz77_output_backref,因为它需要更少的循环迭代和内存访问。实际上,短的反向链接现在将在一个迭代中进行处理,这对于预测分支非常有用。

/* Output the (dist,len) backref at dst_pos in dst using 64-bit wide writes.
   There must be enough room for len bytes rounded to the next multiple of 8. */
static void output_backref64(uint8_t *dst, size_t dst_pos, size_t dist,
                             size_t len)
{
        size_t i;
        uint64_t tmp;

        assert(len > 0);
        assert(dist <= dst_pos && "cannot reference before beginning of dst");

        if (len > dist) {
                /* Self-overlapping backref; fall back to byte-by-byte copy. */
                lz77_output_backref(dst, dst_pos, dist, len);
                return;
        }

        i = 0;
        do {
                memcpy(&tmp, &dst[dst_pos - dist + i], 8);
                memcpy(&dst[dst_pos + i], &tmp, 8);
                i += 8;
        } while (i < len);
}

使用动态霍夫曼代码缩小块


使用动态霍夫曼代码缩小块的工作方式与上述相同。但是,它们使用的是存储在块开头的Deflate流本身中的代码,而不是litlen和dist的预定代码。这个名称可能不成功,因为在编码过程中更改的代码也称为动态霍夫曼代码-这是自适应霍夫曼编码。此处描述的代码与该过程无关。它们仅在不同块可以使用不同代码的意义上是动态的。

生成动态litlen和dist代码是Deflate格式中最困难的部分。但是,一旦生成代码,便以与上一部分相同的方式执行解压缩,方法是inf_block

static inf_stat_t inf_dyn_block(istream_t *is, uint8_t *dst,
                                size_t dst_cap, size_t *dst_pos)
{
        inf_stat_t s;
        huffman_decoder_t litlen_dec, dist_dec;

        s = init_dyn_decoders(is, &litlen_dec, &dist_dec);
        if (s != HWINF_OK) {
                return s;
        }

        return inf_block(is, dst, dst_cap, dst_pos, &litlen_dec, &dist_dec);
}

动态Deflate块的Litlen和dist代码存储为一系列代码字长度。长度本身使用第三霍夫曼代码-codelen进行编码该代码由codelen_lens存储在块中的代码字(的长度确定(我是否提到这很困难?)。


在动态块的开头,有14位确定需要从该块读取的代码字的长度,长度和编码长度的数量:

#define MIN_CODELEN_LENS 4
#define MAX_CODELEN_LENS 19

#define MIN_LITLEN_LENS 257
#define MAX_LITLEN_LENS 288

#define MIN_DIST_LENS 1
#define MAX_DIST_LENS 32

#define CODELEN_MAX_LIT 15

#define CODELEN_COPY 16
#define CODELEN_COPY_MIN 3
#define CODELEN_COPY_MAX 6

#define CODELEN_ZEROS 17
#define CODELEN_ZEROS_MIN 3
#define CODELEN_ZEROS_MAX 10

#define CODELEN_ZEROS2 18
#define CODELEN_ZEROS2_MIN 11
#define CODELEN_ZEROS2_MAX 138

/* RFC 1951, 3.2.7 */
static const int codelen_lengths_order[MAX_CODELEN_LENS] =
{ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };

static inf_stat_t init_dyn_decoders(istream_t *is,
                                    huffman_decoder_t *litlen_dec,
                                    huffman_decoder_t *dist_dec)
{
        uint64_t bits;
        size_t num_litlen_lens, num_dist_lens, num_codelen_lens;
        uint8_t codelen_lengths[MAX_CODELEN_LENS];
        uint8_t code_lengths[MAX_LITLEN_LENS + MAX_DIST_LENS];
        size_t i, n, used;
        int sym;
        huffman_decoder_t codelen_dec;

        bits = istream_bits(is);

        /* Number of litlen codeword lengths (5 bits + 257). */
        num_litlen_lens = lsb(bits, 5) + MIN_LITLEN_LENS;
        bits >>= 5;
        assert(num_litlen_lens <= MAX_LITLEN_LENS);

        /* Number of dist codeword lengths (5 bits + 1). */
        num_dist_lens = lsb(bits, 5) + MIN_DIST_LENS;
        bits >>= 5;
        assert(num_dist_lens <= MAX_DIST_LENS);

        /* Number of code length lengths (4 bits + 4). */
        num_codelen_lens = lsb(bits, 4) + MIN_CODELEN_LENS;
        bits >>= 4;
        assert(num_codelen_lens <= MAX_CODELEN_LENS);

        if (!istream_advance(is, 5 + 5 + 4)) {
                return HWINF_ERR;
        }

然后是codelen代码的代码字长度。这些长度是通常的三位值,但以中指定的特殊顺序写入codelen_lengths_order由于必须确定19个长度,因此只能从流中读取num_codelen_lens其他所有内容都隐式为null。长度以一定顺序列出,因此零长度更有可能落在列表的末尾,而不存储在块中。

       /* Read the codelen codeword lengths (3 bits each)
           and initialize the codelen decoder. */
        for (i = 0; i < num_codelen_lens; i++) {
                bits = istream_bits(is);
                codelen_lengths[codelen_lengths_order[i]] =
                        (uint8_t)lsb(bits, 3);
                if (!istream_advance(is, 3)) {
                        return HWINF_ERR;
                }
        }
        for (; i < MAX_CODELEN_LENS; i++) {
                codelen_lengths[codelen_lengths_order[i]] = 0;
        }
        if (!huffman_decoder_init(&codelen_dec, codelen_lengths,
                                  MAX_CODELEN_LENS)) {
                return HWINF_ERR;
        }

通过设置codelen解码器,我们可以从流中读取被加密的代码字和dist的长度。

       /* Read the litlen and dist codeword lengths. */
        i = 0;
        while (i < num_litlen_lens + num_dist_lens) {
                bits = istream_bits(is);
                sym = huffman_decode(&codelen_dec, (uint16_t)bits, &used);
                bits >>= used;
                if (!istream_advance(is, used)) {
                        return HWINF_ERR;
                }

                if (sym >= 0 && sym <= CODELEN_MAX_LIT) {
                        /* A literal codeword length. */
                        code_lengths[i++] = (uint8_t)sym;
                }

16、17和18不是实际长度,它们指示先前的长度需要重复多次,或者您需要重复零长度:

               else if (sym == CODELEN_COPY) {
                        /* Copy the previous codeword length 3--6 times. */
                        if (i < 1) {
                                return HWINF_ERR; /* No previous length. */
                        }
                        n = lsb(bits, 2) + CODELEN_COPY_MIN; /* 2 bits + 3 */
                        if (!istream_advance(is, 2)) {
                                return HWINF_ERR;
                        }
                        assert(n >= CODELEN_COPY_MIN && n <= CODELEN_COPY_MAX);
                        if (i + n > num_litlen_lens + num_dist_lens) {
                                return HWINF_ERR;
                        }
                        while (n--) {
                                code_lengths[i] = code_lengths[i - 1];
                                i++;
                        }
                } else if (sym == CODELEN_ZEROS) {
                        /* 3--10 zeros. */
                        n = lsb(bits, 3) + CODELEN_ZEROS_MIN; /* 3 bits + 3 */
                        if (!istream_advance(is, 3)) {
                                return HWINF_ERR;
                        }
                        assert(n >= CODELEN_ZEROS_MIN &&
                               n <= CODELEN_ZEROS_MAX);
                        if (i + n > num_litlen_lens + num_dist_lens) {
                                return HWINF_ERR;
                        }
                        while (n--) {
                                code_lengths[i++] = 0;
                        }
                } else if (sym == CODELEN_ZEROS2) {
                        /* 11--138 zeros. */
                        n = lsb(bits, 7) + CODELEN_ZEROS2_MIN; /* 7 bits +138 */
                        if (!istream_advance(is, 7)) {
                                return HWINF_ERR;
                        }
                        assert(n >= CODELEN_ZEROS2_MIN &&
                               n <= CODELEN_ZEROS2_MAX);
                        if (i + n > num_litlen_lens + num_dist_lens) {
                                return HWINF_ERR;
                        }
                        while (n--) {
                                code_lengths[i++] = 0;
                        }
                } else {
                        /* Invalid symbol. */
                        return HWINF_ERR;
                }
        }

请注意,将litlen和dist长度一一读取到数组中code_lengths不能分开读取它们,因为代码长度范围可以从最后的长度延长到第一个dist长度。

准备好代码字长度后,我们可以配置霍夫曼解码器,并返回解码文字和反向链接的任务:

       if (!huffman_decoder_init(litlen_dec, &code_lengths[0],
                                  num_litlen_lens)) {
                return HWINF_ERR;
        }
        if (!huffman_decoder_init(dist_dec, &code_lengths[num_litlen_lens],
                                  num_dist_lens)) {
                return HWINF_ERR;
        }

        return HWINF_OK;
}

压缩(通缩)


在前面的部分中,我们创建了Deflate压缩所需的所有工具:Lempel-Ziv,Huffman编码,位流以及对Deflate三种类型的块的描述。在这一部分中,我们将把它们放在一起以获得Deflate压缩。

压缩Lempel-Ziv将源数据解析为一系列反向链接和文字。如上一部分所述,必须将此序列划分并编码为Deflate块。选择分区方法通常称为阻塞。一方面,每个新块意味着某种开销,其开销取决于块的类型及其内容。更少的块-更少的开销。另一方面,创建新块的这些成本可以得到回报。例如,如果数据的特性使您可以更有效地执行霍夫曼编码并减少生成的数据总量。

阻塞是一项困难的优化任务。一些压缩器(例如Zopfli)比其他压缩器要好一些,但大多数压缩器只是使用贪婪的方法:一旦达到一定大小,它们就会发出块。

不同类型的块具有各自的大小限制:

  • 未压缩的块最多可以包含65,535个字节。
  • Huffman固定代码没有最大大小。
  • 动态霍夫曼代码通常没有最大大小,但是由于霍夫曼算法的实现使用16位字符序列,因此我们限制为65,535个字符。

要自由使用任何类型的块,请将其大小限制为65,534字节:

/* The largest number of bytes that will fit in any kind of block is 65,534.
   It will fit in an uncompressed block (max 65,535 bytes) and a Huffman
   block with only literals (65,535 symbols including end-of-block marker). */
#define MAX_BLOCK_LEN_BYTES 65534

为了在压缩期间跟踪输出比特流和当前块的内容,我们将使用以下结构:

typedef struct deflate_state_t deflate_state_t;
struct deflate_state_t {
        ostream_t os;

        const uint8_t *block_src; /* First src byte in the block. */

        size_t block_len;       /* Number of symbols in the current block. */
        size_t block_len_bytes; /* Number of src bytes in the block. */

        /* Symbol frequencies for the current block. */
        uint16_t litlen_freqs[LITLEN_MAX + 1];
        uint16_t dist_freqs[DISTSYM_MAX + 1];

        struct {
                uint16_t distance;    /* Backref distance. */
                union {
                        uint16_t lit; /* Literal byte or end-of-block. */
                        uint16_t len; /* Backref length (distance != 0). */
                } u;
        } block[MAX_BLOCK_LEN_BYTES + 1];
};

static void reset_block(deflate_state_t *s)
{
        s->block_len = 0;
        s->block_len_bytes = 0;
        memset(s->litlen_freqs, 0, sizeof(s->litlen_freqs));
        memset(s->dist_freqs, 0, sizeof(s->dist_freqs));
}

要将工作结果添加到块中,lz77_compress我们将使用回调函数,并在达到最大大小后,将块写入位流:

static bool lit_callback(uint8_t lit, void *aux)
{
        deflate_state_t *s = aux;

        if (s->block_len_bytes + 1 > MAX_BLOCK_LEN_BYTES) {
                if (!write_block(s, false)) {
                        return false;
                }
                s->block_src += s->block_len_bytes;
                reset_block(s);
        }

        assert(s->block_len < sizeof(s->block) / sizeof(s->block[0]));
        s->block[s->block_len  ].distance = 0;
        s->block[s->block_len++].u.lit = lit;
        s->block_len_bytes++;

        s->litlen_freqs[lit]++;

        return true;
}

static bool backref_callback(size_t dist, size_t len, void *aux)
{
        deflate_state_t *s = aux;

        if (s->block_len_bytes + len > MAX_BLOCK_LEN_BYTES) {
                if (!write_block(s, false)) {
                        return false;
                }
                s->block_src += s->block_len_bytes;
                reset_block(s);
        }

        assert(s->block_len < sizeof(s->block) / sizeof(s->block[0]));
        s->block[s->block_len  ].distance = (uint16_t)dist;
        s->block[s->block_len++].u.len = (uint16_t)len;
        s->block_len_bytes += len;

        assert(len >= MIN_LEN && len <= MAX_LEN);
        assert(dist >= MIN_DISTANCE && dist <= MAX_DISTANCE);
        s->litlen_freqs[len2litlen[len]]++;
        s->dist_freqs[distance2dist[dist]]++;

        return true;
}

最有趣的是记录块。如果该块未压缩,则一切都很简单:

static bool write_uncomp_block(deflate_state_t *s, bool final)
{
        uint8_t len_nlen[4];

        /* Write the block header. */
        if (!ostream_write(&s->os, (0x0 << 1) | final, 3)) {
                return false;
        }

        len_nlen[0] = (uint8_t)(s->block_len_bytes >> 0);
        len_nlen[1] = (uint8_t)(s->block_len_bytes >> 8);
        len_nlen[2] = ~len_nlen[0];
        len_nlen[3] = ~len_nlen[1];

        if (!ostream_write_bytes_aligned(&s->os, len_nlen, sizeof(len_nlen))) {
                return false;
        }

        if (!ostream_write_bytes_aligned(&s->os, s->block_src,
                                         s->block_len_bytes)) {
                return false;
        }

        return true;
}

为了编写静态霍夫曼块,我们首先基于固定的码字长度为碎纸和碎纸代码生成规范代码。然后,我们迭代该块,写下使用以下代码的字符:

static bool write_static_block(deflate_state_t *s, bool final)
{
        huffman_encoder_t litlen_enc, dist_enc;

        /* Write the block header. */
        if (!ostream_write(&s->os, (0x1 << 1) | final, 3)) {
                return false;
        }

        huffman_encoder_init2(&litlen_enc, fixed_litlen_lengths,
                              sizeof(fixed_litlen_lengths) /
                              sizeof(fixed_litlen_lengths[0]));
        huffman_encoder_init2(&dist_enc, fixed_dist_lengths,
                              sizeof(fixed_dist_lengths) /
                              sizeof(fixed_dist_lengths[0]));

        return write_huffman_block(s, &litlen_enc, &dist_enc);
}

static bool write_huffman_block(deflate_state_t *s,
                                const huffman_encoder_t *litlen_enc,
                                const huffman_encoder_t *dist_enc)
{
        size_t i, nbits;
        uint64_t distance, dist, len, litlen, bits, ebits;

        for (i = 0; i < s->block_len; i++) {
                if (s->block[i].distance == 0) {
                        /* Literal or EOB. */
                        litlen = s->block[i].u.lit;
                        assert(litlen <= LITLEN_EOB);
                        if (!ostream_write(&s->os,
                                           litlen_enc->codewords[litlen],
                                           litlen_enc->lengths[litlen])) {
                                return false;
                        }
                        continue;
                }

                /* Back reference length. */
                len = s->block[i].u.len;
                litlen = len2litlen[len];

                /* litlen bits */
                bits = litlen_enc->codewords[litlen];
                nbits = litlen_enc->lengths[litlen];

                /* ebits */
                ebits = len - litlen_tbl[litlen - LITLEN_TBL_OFFSET].base_len;
                bits |= ebits << nbits;
                nbits += litlen_tbl[litlen - LITLEN_TBL_OFFSET].ebits;

                /* Back reference distance. */
                distance = s->block[i].distance;
                dist = distance2dist[distance];

                /* dist bits */
                bits |= (uint64_t)dist_enc->codewords[dist] << nbits;
                nbits += dist_enc->lengths[dist];

                /* ebits */
                ebits = distance - dist_tbl[dist].base_dist;
                bits |= ebits << nbits;
                nbits += dist_tbl[dist].ebits;

                if (!ostream_write(&s->os, bits, nbits)) {
                        return false;
                }
        }

        return true;
}

当然,编写霍夫曼动态块会更困难,因为它们包含棘手的litlen和dist代码编码。为了表示这种编码,我们使用以下结构:

typedef struct codelen_sym_t codelen_sym_t;
struct codelen_sym_t {
        uint8_t sym;
        uint8_t count; /* For symbols 16, 17, 18. */
};

首先,我们丢弃litlen和dist码字的零长度尾部,然后将它们复制到常规数组中以进行后续编码。我们不能丢弃所有零:如果其中没有单个dist代码,则不可能对Deflate块进行编码。少于257个花粉代码也是不可能的,但是由于我们总是有一个字节结束标记,因此对于256个字符,总会有一个非零的代码长度。

/* Encode litlen_lens and dist_lens into encoded. *num_litlen_lens and
   *num_dist_lens will be set to the number of encoded litlen and dist lens,
   respectively. Returns the number of elements in encoded. */
static size_t encode_dist_litlen_lens(const uint8_t *litlen_lens,
                                      const uint8_t *dist_lens,
                                      codelen_sym_t *encoded,
                                      size_t *num_litlen_lens,
                                      size_t *num_dist_lens)
{
        size_t i, n;
        uint8_t lens[LITLEN_MAX + 1 + DISTSYM_MAX + 1];

        *num_litlen_lens = LITLEN_MAX + 1;
        *num_dist_lens = DISTSYM_MAX + 1;

        /* Drop trailing zero litlen lengths. */
        assert(litlen_lens[LITLEN_EOB] != 0 && "EOB len should be non-zero.");
        while (litlen_lens[*num_litlen_lens - 1] == 0) {
                (*num_litlen_lens)--;
        }
        assert(*num_litlen_lens >= MIN_LITLEN_LENS);

        /* Drop trailing zero dist lengths, keeping at least one. */
        while (dist_lens[*num_dist_lens - 1] == 0 && *num_dist_lens > 1) {
                (*num_dist_lens)--;
        }
        assert(*num_dist_lens >= MIN_DIST_LENS);

        /* Copy the lengths into a unified array. */
        n = 0;
        for (i = 0; i < *num_litlen_lens; i++) {
                lens[n++] = litlen_lens[i];
        }
        for (i = 0; i < *num_dist_lens; i++) {
                lens[n++] = dist_lens[i];
        }

        return encode_lens(lens, n, encoded);
}

将代码长度添加到一个数组中后,我们使用特殊字符执行编码以运行相同的代码长度。

/* Encode the n code lengths in lens into encoded, returning the number of
   elements in encoded. */
static size_t encode_lens(const uint8_t *lens, size_t n, codelen_sym_t *encoded)
{
        size_t i, j, num_encoded;
        uint8_t count;

        i = 0;
        num_encoded = 0;
        while (i < n) {
                if (lens[i] == 0) {
                        /* Scan past the end of this zero run (max 138). */
                        for (j = i; j < min(n, i + CODELEN_ZEROS2_MAX) &&
                                    lens[j] == 0; j++);
                        count = (uint8_t)(j - i);

                        if (count < CODELEN_ZEROS_MIN) {
                                /* Output a single zero. */
                                encoded[num_encoded++].sym = 0;
                                i++;
                                continue;
                        }

                        /* Output a repeated zero. */
                        if (count <= CODELEN_ZEROS_MAX) {
                                /* Repeated zero 3--10 times. */
                                assert(count >= CODELEN_ZEROS_MIN &&
                                       count <= CODELEN_ZEROS_MAX);
                                encoded[num_encoded].sym = CODELEN_ZEROS;
                                encoded[num_encoded++].count = count;
                        } else {
                                /* Repeated zero 11--138 times. */
                                assert(count >= CODELEN_ZEROS2_MIN &&
                                       count <= CODELEN_ZEROS2_MAX);
                                encoded[num_encoded].sym = CODELEN_ZEROS2;
                                encoded[num_encoded++].count = count;
                        }
                        i = j;
                        continue;
                }

                /* Output len. */
                encoded[num_encoded++].sym = lens[i++];

                /* Scan past the end of the run of this len (max 6). */
                for (j = i; j < min(n, i + CODELEN_COPY_MAX) &&
                            lens[j] == lens[i - 1]; j++);
                count = (uint8_t)(j - i);

                if (count >= CODELEN_COPY_MIN) {
                        /* Repeat last len 3--6 times. */
                        assert(count >= CODELEN_COPY_MIN &&
                               count <= CODELEN_COPY_MAX);
                        encoded[num_encoded].sym = CODELEN_COPY;
                        encoded[num_encoded++].count = count;
                        i = j;
                        continue;
                }
        }

        return num_encoded;
}

用于编码的字符将使用霍夫曼代码-codelen记录。来自codelen代码的代码字的长度以特定顺序写入到块中,因此零长度更有可能最终结束。这是一个计算应写入多少长度的函数:

static const int codelen_lengths_order[19] =
{ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };

/* Count the number of significant (not trailing zeros) codelen lengths. */
size_t count_codelen_lens(const uint8_t *codelen_lens)
{
        size_t n = MAX_CODELEN_LENS;

        /* Drop trailing zero lengths. */
        while (codelen_lens[codelen_lengths_order[n - 1]] == 0) {
                n--;
        }

        /* The first 4 lengths in the order (16, 17, 18, 0) cannot be used to
           encode any non-zero lengths. Since there will always be at least
           one non-zero codeword length (for EOB), n will be >= 4. */
        assert(n >= MIN_CODELEN_LENS && n <= MAX_CODELEN_LENS);

        return n;
}

假设我们已经设置了litlen和dist代码,设置了它们的代码字长度的编码以及这些长度的代码。现在我们可以编写一个动态的霍夫曼块:

static bool write_dynamic_block(deflate_state_t *s, bool final,
                                size_t num_litlen_lens, size_t num_dist_lens,
                                size_t num_codelen_lens,
                                const huffman_encoder_t *codelen_enc,
                                const codelen_sym_t *encoded_lens,
                                size_t num_encoded_lens,
                                const huffman_encoder_t *litlen_enc,
                                const huffman_encoder_t *dist_enc)
{
        size_t i;
        uint8_t codelen, sym;
        size_t nbits;
        uint64_t bits, hlit, hdist, hclen, count;

        /* Block header. */
        bits = (0x2 << 1) | final;
        nbits = 3;

        /* hlit (5 bits) */
        hlit = num_litlen_lens - MIN_LITLEN_LENS;
        bits |= hlit << nbits;
        nbits += 5;

        /* hdist (5 bits) */
        hdist = num_dist_lens - MIN_DIST_LENS;
        bits |= hdist << nbits;
        nbits += 5;

        /* hclen (4 bits) */
        hclen = num_codelen_lens - MIN_CODELEN_LENS;
        bits |= hclen << nbits;
        nbits += 4;

        if (!ostream_write(&s->os, bits, nbits)) {
                return false;
        }

        /* Codelen lengths. */
        for (i = 0; i < num_codelen_lens; i++) {
                codelen = codelen_enc->lengths[codelen_lengths_order[i]];
                if (!ostream_write(&s->os, codelen, 3)) {
                        return false;
                }
        }

        /* Litlen and dist code lengths. */
        for (i = 0; i < num_encoded_lens; i++) {
                sym = encoded_lens[i].sym;

                bits = codelen_enc->codewords[sym];
                nbits = codelen_enc->lengths[sym];

                count = encoded_lens[i].count;
                if (sym == CODELEN_COPY) { /* 2 ebits */
                        bits |= (count - CODELEN_COPY_MIN) << nbits;
                        nbits += 2;
                } else if (sym == CODELEN_ZEROS) { /* 3 ebits */
                        bits |= (count - CODELEN_ZEROS_MIN) << nbits;
                        nbits += 3;
                } else if (sym == CODELEN_ZEROS2) { /* 7 ebits */
                        bits |= (count - CODELEN_ZEROS2_MIN) << nbits;
                        nbits += 7;
                }

                if (!ostream_write(&s->os, bits, nbits)) {
                        return false;
                }
        }

        return write_huffman_block(s, litlen_enc, dist_enc);
}

对于每个块,我们要使用需要最少位数的类型。可以快速计算出未压缩块的长度:

/* Calculate the number of bits for an uncompressed block, including header. */
static size_t uncomp_block_len(const deflate_state_t *s)
{
        size_t bit_pos, padding;

        /* Bit position after writing the block header. */
        bit_pos = ostream_bit_pos(&s->os) + 3;
        padding = round_up(bit_pos, 8) - bit_pos;

        /* Header + padding + len/nlen + block contents. */
        return 3 + padding + 2 * 16 + s->block_len_bytes * 8;
}

对于霍夫曼编码的块,您可以使用字符的线性度和离散度以及代码字长度来计算主体长度:

/* Calculate the number of bits for a Huffman encoded block body. */
static size_t huffman_block_body_len(const deflate_state_t *s,
                                     const uint8_t *litlen_lens,
                                     const uint8_t *dist_lens)
{
        size_t i, freq, len;

        len = 0;

        for (i = 0; i <= LITLEN_MAX; i++) {
                freq = s->litlen_freqs[i];
                len += litlen_lens[i] * freq;

                if (i >= LITLEN_TBL_OFFSET) {
                        len += litlen_tbl[i - LITLEN_TBL_OFFSET].ebits * freq;
                }
        }

        for (i = 0; i <= DISTSYM_MAX; i++) {
                freq = s->dist_freqs[i];
                len += dist_lens[i] * freq;
                len += dist_tbl[i].ebits * freq;
        }

        return len;
}

静态块的总长度为标头的3位加上主体的长度。计算动态块的头大小需要做更多的工作:

/* Calculate the number of bits for a dynamic Huffman block. */
static size_t dyn_block_len(const deflate_state_t *s, size_t num_codelen_lens,
                            const uint16_t *codelen_freqs,
                            const huffman_encoder_t *codelen_enc,
                            const huffman_encoder_t *litlen_enc,
                            const huffman_encoder_t *dist_enc)
{
        size_t len, i, freq;

        /* Block header. */
        len = 3;

        /* Nbr of litlen, dist, and codelen lengths. */
        len += 5 + 5 + 4;

        /* Codelen lengths. */
        len += 3 * num_codelen_lens;

        /* Codelen encoding. */
        for (i = 0; i < MAX_CODELEN_LENS; i++) {
                freq = codelen_freqs[i];
                len += codelen_enc->lengths[i] * freq;

                /* Extra bits. */
                if (i == CODELEN_COPY) {
                        len += 2 * freq;
                } else if (i == CODELEN_ZEROS) {
                        len += 3 * freq;
                } else if (i == CODELEN_ZEROS2) {
                        len += 7 * freq;
                }
        }

        return len + huffman_block_body_len(s, litlen_enc->lengths,
                                            dist_enc->lengths);
}

现在,我们将所有内容放在一起,并创建用于编写块的主要功能:

/* Write the current deflate block, marking it final if that parameter is true,
   returning false if there is not enough room in the output stream. */
static bool write_block(deflate_state_t *s, bool final)
{
        size_t old_bit_pos, uncomp_len, static_len, dynamic_len;
        huffman_encoder_t dyn_litlen_enc, dyn_dist_enc, codelen_enc;
        size_t num_encoded_lens, num_litlen_lens, num_dist_lens;
        codelen_sym_t encoded_lens[LITLEN_MAX + 1 + DISTSYM_MAX + 1];
        uint16_t codelen_freqs[MAX_CODELEN_LENS] = {0};
        size_t num_codelen_lens;
        size_t i;

        old_bit_pos = ostream_bit_pos(&s->os);

        /* Add the end-of-block marker in case we write a Huffman block. */
        assert(s->block_len < sizeof(s->block) / sizeof(s->block[0]));
        assert(s->litlen_freqs[LITLEN_EOB] == 0);
        s->block[s->block_len  ].distance = 0;
        s->block[s->block_len++].u.lit = LITLEN_EOB;
        s->litlen_freqs[LITLEN_EOB] = 1;

        uncomp_len = uncomp_block_len(s);

        static_len = 3 + huffman_block_body_len(s, fixed_litlen_lengths,
                                                fixed_dist_lengths);

        /* Compute "dynamic" Huffman codes. */
        huffman_encoder_init(&dyn_litlen_enc, s->litlen_freqs,
                             LITLEN_MAX + 1, 15);
        huffman_encoder_init(&dyn_dist_enc, s->dist_freqs, DISTSYM_MAX + 1, 15);

        /* Encode the litlen and dist code lengths. */
        num_encoded_lens = encode_dist_litlen_lens(dyn_litlen_enc.lengths,
                                                   dyn_dist_enc.lengths,
                                                   encoded_lens,
                                                   &num_litlen_lens,
                                                   &num_dist_lens);

        /* Compute the codelen code. */
        for (i = 0; i < num_encoded_lens; i++) {
                codelen_freqs[encoded_lens[i].sym]++;
        }
        huffman_encoder_init(&codelen_enc, codelen_freqs, MAX_CODELEN_LENS, 7);
        num_codelen_lens = count_codelen_lens(codelen_enc.lengths);

        dynamic_len = dyn_block_len(s, num_codelen_lens, codelen_freqs,
                                    &codelen_enc, &dyn_litlen_enc,
                                    &dyn_dist_enc);

        if (uncomp_len <= dynamic_len && uncomp_len <= static_len) {
                if (!write_uncomp_block(s, final)) {
                        return false;
                }
                assert(ostream_bit_pos(&s->os) - old_bit_pos == uncomp_len);
        } else if (static_len <= dynamic_len) {
                if (!write_static_block(s, final)) {
                        return false;
                }
                assert(ostream_bit_pos(&s->os) - old_bit_pos == static_len);
        } else {
                if (!write_dynamic_block(s, final, num_litlen_lens,
                                         num_dist_lens, num_codelen_lens,
                                         &codelen_enc, encoded_lens,
                                         num_encoded_lens, &dyn_litlen_enc,
                                         &dyn_dist_enc)) {
                        return false;
                }
                assert(ostream_bit_pos(&s->os) - old_bit_pos == dynamic_len);
        }

        return true;
}

最后,整个压缩过程的发起者应设置初始状态,开始Lempel-Ziv压缩并写入结果块:

/* Compress (deflate) the data in src into dst. The number of bytes output, at
   most dst_cap, is stored in *dst_used. Returns false if there is not enough
   room in dst. src and dst must not overlap. */
bool hwdeflate(const uint8_t *src, size_t src_len, uint8_t *dst,
               size_t dst_cap, size_t *dst_used)
{
        deflate_state_t s;

        ostream_init(&s.os, dst, dst_cap);
        reset_block(&s);
        s.block_src = src;

        if (!lz77_compress(src, src_len, &lit_callback,
                           &backref_callback, &s)) {
                return false;
        }

        if (!write_block(&s, true)) {
                return false;
        }

        /* The end of the final block should match the end of src. */
        assert(s.block_src + s.block_len_bytes == src + src_len);

        *dst_used = ostream_bytes_written(&s.os);

        return true;
}

压缩文件格式


上面,我们检查了Zip文件中使用的Deflate压缩的工作方式。那文件格式本身呢?在这一部分中,我们将详细研究其结构和实现。该代码位于zip.hzip.c中

总览


文件格式在PKZip 应用说明中描述

  1. zip文件中的每个文件或存档项目都有一个本地文件头,其中包含有关该项目的元数据。
  2. . , , , Zip-.
  3. , . , . Zip-.


每个存档项目均被压缩并单独存储。这意味着,即使存档中的文件之间存在匹配项,也不会考虑使用它们来提高压缩率。

中央目录的末尾位置使您可以逐步完成归档。压缩文件元素后,它们将被添加到存档中。索引是在所有压缩大小之后记录的,这使您可以找出所有文件的偏移量。将文件添加到现有存档非常容易,将其放置在最后一个元素之后,并且覆盖中央目录。

逐渐创建存档的能力对于在众多软盘或卷上传播信息特别重要。压缩后,PKZip建议用户插入新的磁盘,并将中央目录写入最后一个(最后一个)磁盘。要解压缩多卷归档文件,PKZip首先要求写入最后一张软盘以读取中央目录,然后其余软盘需要提取所请求的文件。

这可能会让您感到惊讶,但是没有规则禁止在存档中包含多个具有相同名称的文件。这在解压缩时可能会引起很多混乱:如果有多个同名文件,应该解压缩哪个文件?反过来,这可能会导致安全问题。由于Android中的“万能钥匙”错误(CVE-2013-4787Black Hat上的报告中的幻灯片视频),攻击者可以在安装程序时绕过加密签名操作系统的检查。 Android程序以APK文件(Zip文件)的形式分发。事实证明,如果APK包含多个具有相同名称的文件,则签名验证码选择了具有相同名称最后一个文件,而安装程序选择了第一个文件,即未执行验证。换句话说,两个Zip库之间的微小差异使绕过整个操作系统安全模型成为可能。

与大多数格式不同,zip文件不应以签名或魔术数字开头。通常没有指示Zip文件应该以任何特定的方式启动,这很容易让您创建同时可用作Zip和不同格式的文件-polyglot文件。例如,自解压的Zip存档(例如pkz204g.exe)通常既是可执行文件又是Zip文件:第一部分是可执行文件,其后是Zip文件(由可执行文件部分解压缩)。操作系统可以将其作为可执行文件执行,但是Zip程序会将其作为Zip文件打开。此功能可能会导致您不需要文件开头的签名。

尽管这样的多语言文件很聪明,但它们可能会导致安全问题,因为它们可以欺骗试图确定文件内容的程序,并且还允许在包含不同类型文件的地方传递恶意代码。例如,利用漏洞利用了GIFAR文件,这些文件同时是有效的GIF图像和Java存档(JAR,一种Zip文件)。有关此问题的更多信息,请参阅文章滥用文件格式(从第18页开始)。

正如我们将在下面看到的那样,Zip文件使用32位字段作为偏移量和大小,以便将归档文件及其元素的大小限制为4 GB。在应用笔记4.5中PKWare添加了格式扩展名,允许使用64位偏移量和大小。使用这些扩展名的文件为Zip64格式,但我们将不考虑它们。

数据结构


中央目录条目的结尾


中央目录条目(EOCDR)的末尾通常用作读取zip文件的起点。它包含中央目录的位置和大小,以及有关整个档案的可选注释。

在占用多个磁盘(或卷)的zip文件中,EOCDR还包含有关我们当前正在使用的磁盘,中央目录在哪个磁盘上启动的信息,等等。今天,很少使用此功能,并且本文中的代码不处理此类文件。

EOCDR由签名'P''K'以及字节5和6决定。其后是下面的结构,数字按照小端顺序存储:

/* End of Central Directory Record. */
struct eocdr {
        uint16_t disk_nbr;        /* Number of this disk. */
        uint16_t cd_start_disk;   /* Nbr. of disk with start of the CD. */
        uint16_t disk_cd_entries; /* Nbr. of CD entries on this disk. */
        uint16_t cd_entries;      /* Nbr. of Central Directory entries. */
        uint32_t cd_size;         /* Central Directory size in bytes. */
        uint32_t cd_offset;       /* Central Directory file offset. */
        uint16_t comment_len;     /* Archive comment length. */
        const uint8_t *comment;   /* Archive comment. */
};

EOCDR应位于文件末尾。但是,由于尾部可能有任意16位长度的注释,因此可能有必要找到特定位置:

/* Read 16/32 bits little-endian and bump p forward afterwards. */
#define READ16(p) ((p) += 2, read16le((p) - 2))
#define READ32(p) ((p) += 4, read32le((p) - 4))

/* Size of the End of Central Directory Record, not including comment. */
#define EOCDR_BASE_SZ 22
#define EOCDR_SIGNATURE 0x06054b50  /* "PK\5\6" little-endian. */

static bool find_eocdr(struct eocdr *r, const uint8_t *src, size_t src_len)
{
        size_t comment_len;
        const uint8_t *p;
        uint32_t signature;

        for (comment_len = 0; comment_len <= UINT16_MAX; comment_len++) {
                if (src_len < EOCDR_BASE_SZ + comment_len) {
                        break;
                }

                p = &src[src_len - EOCDR_BASE_SZ - comment_len];
                signature = READ32(p);

                if (signature == EOCDR_SIGNATURE) {
                        r->disk_nbr = READ16(p);
                        r->cd_start_disk = READ16(p);
                        r->disk_cd_entries = READ16(p);
                        r->cd_entries = READ16(p);
                        r->cd_size = READ32(p);
                        r->cd_offset = READ32(p);
                        r->comment_len = READ16(p);
                        r->comment = p;
                        assert(p == &src[src_len - comment_len] &&
                               "All fields read.");

                        if (r->comment_len == comment_len) {
                                return true;
                        }
                }
        }

        return false;
}

记录EOCDR很容易。该函数写入并返回写入的字节数:

/* Write 16/32 bits little-endian and bump p forward afterwards. */
#define WRITE16(p, x) (write16le((p), (x)), (p) += 2)
#define WRITE32(p, x) (write32le((p), (x)), (p) += 4)

static size_t write_eocdr(uint8_t *dst, const struct eocdr *r)
{
        uint8_t *p = dst;

        WRITE32(p, EOCDR_SIGNATURE);
        WRITE16(p, r->disk_nbr);
        WRITE16(p, r->cd_start_disk);
        WRITE16(p, r->disk_cd_entries);
        WRITE16(p, r->cd_entries);
        WRITE32(p, r->cd_size);
        WRITE32(p, r->cd_offset);
        WRITE16(p, r->comment_len);
        assert(p - dst == EOCDR_BASE_SZ);

        if (r->comment_len != 0) {
                memcpy(p, r->comment, r->comment_len);
                p += r->comment_len;
        }

        return (size_t)(p - dst);
}

中央文件头


中央目录由一个接一个地写入的中央文件头组成,每个归档项目一个。每个标题都以签名“ P”,“ K”,1、2开头,然后是这样的结构:

#define EXT_ATTR_DIR (1U << 4)
#define EXT_ATTR_ARC (1U << 5)

/* Central File Header (Central Directory Entry) */
struct cfh {
        uint16_t made_by_ver;    /* Version made by. */
        uint16_t extract_ver;    /* Version needed to extract. */
        uint16_t gp_flag;        /* General purpose bit flag. */
        uint16_t method;         /* Compression method. */
        uint16_t mod_time;       /* Modification time. */
        uint16_t mod_date;       /* Modification date. */
        uint32_t crc32;          /* CRC-32 checksum. */
        uint32_t comp_size;      /* Compressed size. */
        uint32_t uncomp_size;    /* Uncompressed size. */
        uint16_t name_len;       /* Filename length. */
        uint16_t extra_len;      /* Extra data length. */
        uint16_t comment_len;    /* Comment length. */
        uint16_t disk_nbr_start; /* Disk nbr. where file begins. */
        uint16_t int_attrs;      /* Internal file attributes. */
        uint32_t ext_attrs;      /* External file attributes. */
        uint32_t lfh_offset;     /* Local File Header offset. */
        const uint8_t *name;     /* Filename. */
        const uint8_t *extra;    /* Extra data. */
        const uint8_t *comment;  /* File comment. */
};

made_by_verextract_ver编码有关用于添加此项的操作系统和程序版本的信息,以及检索所需的版本。最重要的八位用于编码操作系统(例如,0表示DOS,3表示Unix,10表示Windows NTFS),而低八位用于编码软件版本。将十进制值设置为20,这意味着与PKZip 2.0兼容。

gp_flag包含不同的标志。我们感兴趣:

  • 位0,指示元素加密的事实(我们不会考虑);
  • 以及位1和2,对Deflate压缩级别进行编码(0-正常,1-最大值,2-快速,3-非常快)。

method编码压缩方法。 0-数据未压缩,8-应用了延迟。其他值与旧算法或新算法有关,但几乎所有Zip都使用这两个值。

mod_timemod_date包含文件修改的日期和时间,以MS-DOS格式编码。使用此代码,我们将通常的C时间戳time_t与MS-DOS格式相互转换:

/* Convert DOS date and time to time_t. */
static time_t dos2ctime(uint16_t dos_date, uint16_t dos_time)
{
        struct tm tm = {0};

        tm.tm_sec = (dos_time & 0x1f) * 2;  /* Bits 0--4:  Secs divided by 2. */
        tm.tm_min = (dos_time >> 5) & 0x3f; /* Bits 5--10: Minute. */
        tm.tm_hour = (dos_time >> 11);      /* Bits 11-15: Hour (0--23). */

        tm.tm_mday = (dos_date & 0x1f);          /* Bits 0--4: Day (1--31). */
        tm.tm_mon = ((dos_date >> 5) & 0xf) - 1; /* Bits 5--8: Month (1--12). */
        tm.tm_year = (dos_date >> 9) + 80;       /* Bits 9--15: Year-1980. */

        tm.tm_isdst = -1;

        return mktime(&tm);
}

/* Convert time_t to DOS date and time. */
static void ctime2dos(time_t t, uint16_t *dos_date, uint16_t *dos_time)
{
        struct tm *tm = localtime(&t);

        *dos_time = 0;
        *dos_time |= tm->tm_sec / 2;    /* Bits 0--4:  Second divided by two. */
        *dos_time |= tm->tm_min << 5;   /* Bits 5--10: Minute. */
        *dos_time |= tm->tm_hour << 11; /* Bits 11-15: Hour. */

        *dos_date = 0;
        *dos_date |= tm->tm_mday;             /* Bits 0--4:  Day (1--31). */
        *dos_date |= (tm->tm_mon + 1) << 5;   /* Bits 5--8:  Month (1--12). */
        *dos_date |= (tm->tm_year - 80) << 9; /* Bits 9--15: Year from 1980. */
}

该字段crc32包含未压缩数据的循环冗余码。它用于在检索后验证数据完整性。此处的实现:crc32.c

comp_sizeuncomp_size包含项目文件数据的压缩和未压缩大小。以下三个字段包含名称,注释和紧随标题之后的其他数据的长度。disk_nbr_start设计用于使用多个软盘的存档。

int_attrsext_attrs描述文件的内部和外部属性。内部的与文件的内容有关,例如,最低有效位指示文件是否仅包含文本。外部属性指示文件是否为隐藏,只读等。这些字段的内容取决于操作系统,尤其取决于made_by_ver。在DOS中,低8位包含文件属性字节,可从Int 21 / AX = 4300h系统调用获得。例如,位4表示它是一个目录,位5表示已设置“存档”属性(对于DOS中的大多数文件为true)。据我了解,出于兼容性考虑,这些位在其他操作系统中也进行了类似设置。在Unix,该字段的高16位包含由返回的文件模式位STAT(2)st_mode

lfh_offset告诉我们在哪里寻找本地文件头。name-文件名(C行),以及comment-此存档元素的可选注释(C行)。extra可能包含可选的附加数据,例如有关Unix文件所有者的信息,更准确的更改日期和时间或Zip64字段。

此函数用于读取文件的中央头文件:

/* Size of a Central File Header, not including name, extra, and comment. */
#define CFH_BASE_SZ 46
#define CFH_SIGNATURE 0x02014b50 /* "PK\1\2" little-endian. */

static bool read_cfh(struct cfh *cfh, const uint8_t *src, size_t src_len,
                     size_t offset)
{
        const uint8_t *p;
        uint32_t signature;

        if (offset > src_len || src_len - offset < CFH_BASE_SZ) {
                return false;
        }

        p = &src[offset];
        signature = READ32(p);
        if (signature != CFH_SIGNATURE) {
                return false;
        }

        cfh->made_by_ver = READ16(p);
        cfh->extract_ver = READ16(p);
        cfh->gp_flag = READ16(p);
        cfh->method = READ16(p);
        cfh->mod_time = READ16(p);
        cfh->mod_date = READ16(p);
        cfh->crc32 = READ32(p);
        cfh->comp_size = READ32(p);
        cfh->uncomp_size = READ32(p);
        cfh->name_len = READ16(p);
        cfh->extra_len = READ16(p);
        cfh->comment_len = READ16(p);
        cfh->disk_nbr_start = READ16(p);
        cfh->int_attrs = READ16(p);
        cfh->ext_attrs = READ32(p);
        cfh->lfh_offset = READ32(p);
        cfh->name = p;
        cfh->extra = cfh->name + cfh->name_len;
        cfh->comment = cfh->extra + cfh->extra_len;
        assert(p == &src[offset + CFH_BASE_SZ] && "All fields read.");

        if (src_len - offset - CFH_BASE_SZ <
            cfh->name_len + cfh->extra_len + cfh->comment_len) {
                return false;
        }

        return true;
}

static size_t write_cfh(uint8_t *dst, const struct cfh *cfh)
{
        uint8_t *p = dst;

        WRITE32(p, CFH_SIGNATURE);
        WRITE16(p, cfh->made_by_ver);
        WRITE16(p, cfh->extract_ver);
        WRITE16(p, cfh->gp_flag);
        WRITE16(p, cfh->method);
        WRITE16(p, cfh->mod_time);
        WRITE16(p, cfh->mod_date);
        WRITE32(p, cfh->crc32);
        WRITE32(p, cfh->comp_size);
        WRITE32(p, cfh->uncomp_size);
        WRITE16(p, cfh->name_len);
        WRITE16(p, cfh->extra_len);
        WRITE16(p, cfh->comment_len);
        WRITE16(p, cfh->disk_nbr_start);
        WRITE16(p, cfh->int_attrs);
        WRITE32(p, cfh->ext_attrs);
        WRITE32(p, cfh->lfh_offset);
        assert(p - dst == CFH_BASE_SZ);

        if (cfh->name_len != 0) {
                memcpy(p, cfh->name, cfh->name_len);
                p += cfh->name_len;
        }

        if (cfh->extra_len != 0) {
                memcpy(p, cfh->extra, cfh->extra_len);
                p += cfh->extra_len;
        }

        if (cfh->comment_len != 0) {
                memcpy(p, cfh->comment, cfh->comment_len);
                p += cfh->comment_len;
        }

        return (size_t)(p - dst);
}

本地文件头


每个归档元素的数据前面都有一个本地文件头,该头重复了来自中心头的大多数信息。

可能是在中央和本地标头中引入了数据复制,以便PKZip在解压缩时不会将整个中央目录保留在内存中。相反,在提取每个文件时,可以从本地标头读取其名称和其他信息。此外,本地头文件对于从中央目录丢失或损坏的Zip存档中恢复文件很有用。

但是,这种冗余也是不确定性的主要来源。例如,如果中央和本地标头中的文件名不匹配怎么办?这通常会导致错误和安全问题。

并非中心标题中的所有信息都是重复的。例如,具有文件属性的字段。此外,如果设置了第三最低有效位gp_flags(CRC-32),则压缩和未压缩的字段将重置为零,并且该信息可以在文件本身的数据之后的数据描述符块中找到(我们将不考虑它)。这样,您就可以在知道元素的文件大小或将其压缩为多少之前记录本地标头。

本地头以签名“ P”,“ K”,3、4开头,然后是这样的结构:

/* Local File Header. */
struct lfh {
        uint16_t extract_ver;
        uint16_t gp_flag;
        uint16_t method;
        uint16_t mod_time;
        uint16_t mod_date;
        uint32_t crc32;
        uint32_t comp_size;
        uint32_t uncomp_size;
        uint16_t name_len;
        uint16_t extra_len;
        const uint8_t *name;
        const uint8_t *extra;
};

这些函数读取和写入本地标头,就像其他数据结构一样:

/* Size of a Local File Header, not including name and extra. */
#define LFH_BASE_SZ 30
#define LFH_SIGNATURE 0x04034b50 /* "PK\3\4" little-endian. */

static bool read_lfh(struct lfh *lfh, const uint8_t *src, size_t src_len,
                     size_t offset)
{
        const uint8_t *p;
        uint32_t signature;

        if (offset > src_len || src_len - offset < LFH_BASE_SZ) {
                return false;
        }

        p = &src[offset];
        signature = READ32(p);
        if (signature != LFH_SIGNATURE) {
                return false;
        }

        lfh->extract_ver = READ16(p);
        lfh->gp_flag = READ16(p);
        lfh->method = READ16(p);
        lfh->mod_time = READ16(p);
        lfh->mod_date = READ16(p);
        lfh->crc32 = READ32(p);
        lfh->comp_size = READ32(p);
        lfh->uncomp_size = READ32(p);
        lfh->name_len = READ16(p);
        lfh->extra_len = READ16(p);
        lfh->name = p;
        lfh->extra = lfh->name + lfh->name_len;
        assert(p == &src[offset + LFH_BASE_SZ] && "All fields read.");

        if (src_len - offset - LFH_BASE_SZ < lfh->name_len + lfh->extra_len) {
                return false;
        }

        return true;
}

static size_t write_lfh(uint8_t *dst, const struct lfh *lfh)
{
        uint8_t *p = dst;

        WRITE32(p, LFH_SIGNATURE);
        WRITE16(p, lfh->extract_ver);
        WRITE16(p, lfh->gp_flag);
        WRITE16(p, lfh->method);
        WRITE16(p, lfh->mod_time);
        WRITE16(p, lfh->mod_date);
        WRITE32(p, lfh->crc32);
        WRITE32(p, lfh->comp_size);
        WRITE32(p, lfh->uncomp_size);
        WRITE16(p, lfh->name_len);
        WRITE16(p, lfh->extra_len);
        assert(p - dst == LFH_BASE_SZ);

        if (lfh->name_len != 0) {
                memcpy(p, lfh->name, lfh->name_len);
                p += lfh->name_len;
        }

        if (lfh->extra_len != 0) {
                memcpy(p, lfh->extra, lfh->extra_len);
                p += lfh->extra_len;
        }

        return (size_t)(p - dst);
}

Zip Read的实现


使用以上功能,我们实现了将Zip文件读入内存并获得用于访问归档元素的迭代器:

typedef size_t zipiter_t; /* Zip archive member iterator. */

typedef struct zip_t zip_t;
struct zip_t {
        uint16_t num_members;    /* Number of members. */
        const uint8_t *comment;  /* Zip file comment (not terminated). */
        uint16_t comment_len;    /* Zip file comment length. */
        zipiter_t members_begin; /* Iterator to the first member. */
        zipiter_t members_end;   /* Iterator to the end of members. */

        const uint8_t *src;
        size_t src_len;
};

/* Initialize zip based on the source data. Returns true on success, or false
   if the data could not be parsed as a valid Zip file. */
bool zip_read(zip_t *zip, const uint8_t *src, size_t src_len)
{
        struct eocdr eocdr;
        struct cfh cfh;
        struct lfh lfh;
        size_t i, offset;
        const uint8_t *comp_data;

        zip->src = src;
        zip->src_len = src_len;

        if (!find_eocdr(&eocdr, src, src_len)) {
                return false;
        }

        if (eocdr.disk_nbr != 0 || eocdr.cd_start_disk != 0 ||
            eocdr.disk_cd_entries != eocdr.cd_entries) {
                return false; /* Cannot handle multi-volume archives. */
        }

        zip->num_members = eocdr.cd_entries;
        zip->comment = eocdr.comment;
        zip->comment_len = eocdr.comment_len;

        offset = eocdr.cd_offset;
        zip->members_begin = offset;

        /* Read the member info and do a few checks. */
        for (i = 0; i < eocdr.cd_entries; i++) {
                if (!read_cfh(&cfh, src, src_len, offset)) {
                        return false;
                }

                if (cfh.gp_flag & 1) {
                        return false; /* The member is encrypted. */
                }
                if (cfh.method != ZIP_STORED && cfh.method != ZIP_DEFLATED) {
                        return false; /* Unsupported compression method. */
                }
                if (cfh.method == ZIP_STORED &&
                    cfh.uncomp_size != cfh.comp_size) {
                        return false;
                }
                if (cfh.disk_nbr_start != 0) {
                        return false; /* Cannot handle multi-volume archives. */
                }
                if (memchr(cfh.name, '\0', cfh.name_len) != NULL) {
                        return false; /* Bad filename. */
                }

                if (!read_lfh(&lfh, src, src_len, cfh.lfh_offset)) {
                        return false;
                }

                comp_data = lfh.extra + lfh.extra_len;
                if (cfh.comp_size > src_len - (size_t)(comp_data - src)) {
                        return false; /* Member data does not fit in src. */
                }

                offset += CFH_BASE_SZ + cfh.name_len + cfh.extra_len +
                          cfh.comment_len;
        }

        zip->members_end = offset;

        return true;
}

如上所述,元素迭代器只是到中央文件头的偏移量,您可以通过它访问元素数据:

typedef enum { ZIP_STORED = 0, ZIP_DEFLATED = 8 } method_t;

typedef struct zipmemb_t zipmemb_t;
struct zipmemb_t {
        const uint8_t *name;      /* Member name (not null terminated). */
        uint16_t name_len;        /* Member name length. */
        time_t mtime;             /* Modification time. */
        uint32_t comp_size;       /* Compressed size. */
        const uint8_t *comp_data; /* Compressed data. */
        method_t method;          /* Compression method. */
        uint32_t uncomp_size;     /* Uncompressed size. */
        uint32_t crc32;           /* CRC-32 checksum. */
        const uint8_t *comment;   /* Comment (not null terminated). */
        uint16_t comment_len;     /* Comment length. */
        bool is_dir;              /* Whether this is a directory. */
        zipiter_t next;           /* Iterator to the next member. */
};

/* Get the Zip archive member through iterator it. */
zipmemb_t zip_member(const zip_t *zip, zipiter_t it)
{
        struct cfh cfh;
        struct lfh lfh;
        bool ok;
        zipmemb_t m;

        assert(it >= zip->members_begin && it < zip->members_end);

        ok = read_cfh(&cfh, zip->src, zip->src_len, it);
        assert(ok);

        ok = read_lfh(&lfh, zip->src, zip->src_len, cfh.lfh_offset);
        assert(ok);

        m.name = cfh.name;
        m.name_len = cfh.name_len;
        m.mtime = dos2ctime(cfh.mod_date, cfh.mod_time);
        m.comp_size = cfh.comp_size;
        m.comp_data = lfh.extra + lfh.extra_len;
        m.method = cfh.method;
        m.uncomp_size = cfh.uncomp_size;
        m.crc32 = cfh.crc32;
        m.comment = cfh.comment;
        m.comment_len = cfh.comment_len;
        m.is_dir = (cfh.ext_attrs & EXT_ATTR_DIR) != 0;

        m.next = it + CFH_BASE_SZ +
                 cfh.name_len + cfh.extra_len + cfh.comment_len;

        assert(m.next <= zip->members_end);

        return m;
}

邮编记录实施


要将zip文件写入内存缓冲区,您首先需要找出要为其分配多少内存。而且由于我们在尝试写入之前不知道会压缩多少数据,因此我们将基于未压缩元素的大小来计算上限:

/* Compute an upper bound on the dst size required by zip_write() for an
 * archive with num_memb members with certain filenames, sizes, and archive
 * comment. Returns zero on error, e.g. if a filename is longer than 2^16-1, or
 * if the total file size is larger than 2^32-1. */
uint32_t zip_max_size(uint16_t num_memb, const char *const *filenames,
                      const uint32_t *file_sizes, const char *comment)
{
        size_t comment_len, name_len;
        uint64_t total;
        uint16_t i;

        comment_len = (comment == NULL ? 0 : strlen(comment));
        if (comment_len > UINT16_MAX) {
                return 0;
        }

        total = EOCDR_BASE_SZ + comment_len; /* EOCDR */

        for (i = 0; i < num_memb; i++) {
                assert(filenames[i] != NULL);
                name_len = strlen(filenames[i]);
                if (name_len > UINT16_MAX) {
                        return 0;
                }

                total += CFH_BASE_SZ + name_len; /* Central File Header */
                total += LFH_BASE_SZ + name_len; /* Local File Header */
                total += file_sizes[i];          /* Uncompressed data size. */
        }

        if (total > UINT32_MAX) {
                return 0;
        }

        return (uint32_t)total;
}

这段代码使用每个元素的deflate压缩写入zip文件,以减小其大小:

/* Write a Zip file containing num_memb members into dst, which must be large
   enough to hold the resulting data. Returns the number of bytes written, which
   is guaranteed to be less than or equal to the result of zip_max_size() when
   called with the corresponding arguments. comment shall be a null-terminated
   string or null. callback shall be null or point to a function which will
   get called after the compression of each member. */
uint32_t zip_write(uint8_t *dst, uint16_t num_memb,
                   const char *const *filenames,
                   const uint8_t *const *file_data,
                   const uint32_t *file_sizes,
                   const time_t *mtimes,
                   const char *comment,
                   void (*callback)(const char *filename, uint32_t size,
                                    uint32_t comp_size))
{
        uint16_t i;
        uint8_t *p;
        struct eocdr eocdr;
        struct cfh cfh;
        struct lfh lfh;
        bool ok;
        uint16_t name_len;
        uint8_t *data_dst;
        size_t comp_sz;
        uint32_t lfh_offset, cd_offset, eocdr_offset;

        p = dst;

        /* Write Local File Headers and deflated or stored data. */
        for (i = 0; i < num_memb; i++) {
                assert(filenames[i] != NULL);
                assert(strlen(filenames[i]) <= UINT16_MAX);
                name_len = (uint16_t)strlen(filenames[i]);

                data_dst = p + LFH_BASE_SZ + name_len;

                if (hwdeflate(file_data[i], file_sizes[i], data_dst,
                              file_sizes[i], &comp_sz) &&
                                comp_sz < file_sizes[i]) {
                        lfh.method = ZIP_DEFLATED;
                        assert(comp_sz <= UINT32_MAX);
                        lfh.comp_size = (uint32_t)comp_sz;
                } else {
                        memcpy(data_dst, file_data[i], file_sizes[i]);
                        lfh.method = ZIP_STORED;
                        lfh.comp_size = file_sizes[i];
                }

                if (callback != NULL) {
                        callback(filenames[i], file_sizes[i], lfh.comp_size);
                }

                lfh.extract_ver = (0 << 8) | 20; /* DOS | PKZIP 2.0 */
                lfh.gp_flag = (lfh.method == ZIP_DEFLATED ? (0x1 << 1) : 0x0);
                ctime2dos(mtimes[i], &lfh.mod_date, &lfh.mod_time);
                lfh.crc32 = crc32(file_data[i], file_sizes[i]);
                lfh.uncomp_size = file_sizes[i];
                lfh.name_len = name_len;
                lfh.extra_len = 0;
                lfh.name = (const uint8_t*)filenames[i];
                p += write_lfh(p, &lfh);
                p += lfh.comp_size;
        }

        assert(p - dst <= UINT32_MAX);
        cd_offset = (uint32_t)(p - dst);

        /* Write the Central Directory based on the Local File Headers. */
        lfh_offset = 0;
        for (i = 0; i < num_memb; i++) {
                ok = read_lfh(&lfh, dst, SIZE_MAX, lfh_offset);
                assert(ok);

                cfh.made_by_ver = lfh.extract_ver;
                cfh.extract_ver = lfh.extract_ver;
                cfh.gp_flag = lfh.gp_flag;
                cfh.method = lfh.method;
                cfh.mod_time = lfh.mod_time;
                cfh.mod_date = lfh.mod_date;
                cfh.crc32 = lfh.crc32;
                cfh.comp_size = lfh.comp_size;
                cfh.uncomp_size = lfh.uncomp_size;
                cfh.name_len = lfh.name_len;
                cfh.extra_len = 0;
                cfh.comment_len = 0;
                cfh.disk_nbr_start = 0;
                cfh.int_attrs = 0;
                cfh.ext_attrs = EXT_ATTR_ARC;
                cfh.lfh_offset = lfh_offset;
                cfh.name = lfh.name;
                p += write_cfh(p, &cfh);

                lfh_offset += LFH_BASE_SZ + lfh.name_len + lfh.comp_size;
        }

        assert(p - dst <= UINT32_MAX);
        eocdr_offset = (uint32_t)(p - dst);

        /* Write the End of Central Directory Record. */
        eocdr.disk_nbr = 0;
        eocdr.cd_start_disk = 0;
        eocdr.disk_cd_entries = num_memb;
        eocdr.cd_entries = num_memb;
        eocdr.cd_size = eocdr_offset - cd_offset;
        eocdr.cd_offset = cd_offset;
        eocdr.comment_len = (uint16_t)(comment == NULL ? 0 : strlen(comment));
        eocdr.comment = (const uint8_t*)comment;
        p += write_eocdr(p, &eocdr);

        assert(p - dst <= zip_max_size(num_memb, filenames, file_sizes,
                                       comment));

        return (uint32_t)(p - dst);
}

压缩文件


现在我们知道了如何读写Zip文件,如何压缩和解压缩存储在其中的数据。现在,编写一个包含所有这些工具的简单zip程序。该代码可从hwzip.c获得

我们将使用宏进行简单的错误处理,并使用一些辅助功能来检查内存分配:

#define PERROR_IF(cond, msg) if (cond) { perror(msg); exit(1); }

static void *xmalloc(size_t size)
{
        void *ptr = malloc(size);
        PERROR_IF(ptr == NULL, "malloc");
        return ptr;
}

static void *xrealloc(void *ptr, size_t size)
{
        ptr = realloc(ptr, size);
        PERROR_IF(ptr == NULL, "realloc");
        return ptr;
}

其他两个功能用于读取和写入文件:

static uint8_t *read_file(const char *filename, size_t *file_sz)
{
        FILE *f;
        uint8_t *buf;
        size_t buf_cap;

        f = fopen(filename, "rb");
        PERROR_IF(f == NULL, "fopen");

        buf_cap = 4096;
        buf = xmalloc(buf_cap);

        *file_sz = 0;
        while (feof(f) == 0) {
                if (buf_cap - *file_sz == 0) {
                        buf_cap *= 2;
                        buf = xrealloc(buf, buf_cap);
                }

                *file_sz += fread(&buf[*file_sz], 1, buf_cap - *file_sz, f);
                PERROR_IF(ferror(f), "fread");
        }

        PERROR_IF(fclose(f) != 0, "fclose");
        return buf;
}

static void write_file(const char *filename, const uint8_t *data, size_t n)
{
        FILE *f;

        f = fopen(filename, "wb");
        PERROR_IF(f == NULL, "fopen");
        PERROR_IF(fwrite(data, 1, n, f) != n, "fwrite");
        PERROR_IF(fclose(f) != 0, "fclose");
}

我们的Zip程序可以执行三个功能:列出Zip文件的内容并进行提取,以及创建Zip文件。清单再简单不过了:

static void list_zip(const char *filename)
{
        uint8_t *zip_data;
        size_t zip_sz;
        zip_t z;
        zipiter_t it;
        zipmemb_t m;

        printf("Listing ZIP archive: %s\n\n", filename);

        zip_data = read_file(filename, &zip_sz);

        if (!zip_read(&z, zip_data, zip_sz)) {
                printf("Failed to parse ZIP file!\n");
                exit(1);
        }

        if (z.comment_len != 0) {
                printf("%.*s\n\n", (int)z.comment_len, z.comment);
        }

        for (it = z.members_begin; it != z.members_end; it = m.next) {
                m = zip_member(&z, it);
                printf("%.*s\n", (int)m.name_len, m.name);
        }

        printf("\n");

        free(zip_data);
}

提取有点复杂。我们将使用辅助函数对文件名进行空终止(将其传递给fopen)并解压缩

static char *terminate_str(const char *str, size_t n)
{
        char *p = xmalloc(n + 1);
        memcpy(p, str, n);
        p[n] = '\0';
        return p;
}

static uint8_t *inflate_member(const zipmemb_t *m)
{
        uint8_t *p;
        size_t src_used, dst_used;

        assert(m->method == ZIP_DEFLATED);

        p = xmalloc(m->uncomp_size);

        if (hwinflate(m->comp_data, m->comp_size, &src_used, p, m->uncomp_size,
                      &dst_used) != HWINF_OK) {
                free(p);
                return NULL;
        }

        if (src_used != m->comp_size || dst_used != m->uncomp_size) {
                free(p);
                return NULL;
        }

        return p;
}

我们的程序将跳过具有目录的所有存档元素。这样做是为了避免所谓的路径遍历攻击:恶意存档用于从用户指定的目录外部写入文件。阅读Info-ZIP FAQ中的详细信息

static void extract_zip(const char *filename)
{
        uint8_t *zip_data;
        size_t zip_sz;
        zip_t z;
        zipiter_t it;
        zipmemb_t m;
        char *tname;
        uint8_t *inflated;
        const uint8_t *uncomp_data;

        printf("Extracting ZIP archive: %s\n\n", filename);

        zip_data = read_file(filename, &zip_sz);

        if (!zip_read(&z, zip_data, zip_sz)) {
                printf("Failed to read ZIP file!\n");
                exit(1);
        }

        if (z.comment_len != 0) {
                printf("%.*s\n\n", (int)z.comment_len, z.comment);
        }

        for (it = z.members_begin; it != z.members_end; it = m.next) {
                m = zip_member(&z, it);

                if (m.is_dir) {
                        printf(" (Skipping dir: %.*s)\n",
                               (int)m.name_len, m.name);
                        continue;
                }

                if (memchr(m.name, '/',  m.name_len) != NULL ||
                    memchr(m.name, '\\', m.name_len) != NULL) {
                        printf(" (Skipping file in dir: %.*s)\n",
                               (int)m.name_len, m.name);
                        continue;
                }

                assert(m.method == ZIP_STORED || m.method == ZIP_DEFLATED);
                printf(" %s: %.*s",
                       m.method == ZIP_STORED ? "Extracting" : " Inflating",
                       (int)m.name_len, m.name);
                fflush(stdout);

                if (m.method == ZIP_STORED) {
                        assert(m.uncomp_size == m.comp_size);
                        inflated = NULL;
                        uncomp_data = m.comp_data;
                } else {
                        inflated = inflate_member(&m);
                        if (inflated == NULL) {
                                printf("Error: inflation failed!\n");
                                exit(1);
                        }
                        uncomp_data = inflated;
                }

                if (crc32(uncomp_data, m.uncomp_size) != m.crc32) {
                        printf("Error: CRC-32 mismatch!\n");
                        exit(1);
                }

                tname = terminate_str((const char*)m.name, m.name_len);
                write_file(tname, uncomp_data, m.uncomp_size);
                printf("\n");

                free(inflated);
                free(tname);
        }

        printf("\n");
        free(zip_data);
}

要创建一个zip归档文件,我们将读取输入文件并将其输入zip_write由于标准C库不允许您获取文件修改时间,因此我们将使用当前时间(我将其留作功课来修复此功能)。

void zip_callback(const char *filename, uint32_t size, uint32_t comp_size)
{
        bool deflated = comp_size < size;

        printf(" %s: %s", deflated ? "Deflated" : "  Stored", filename);
        if (deflated) {
                printf(" (%u%%)", 100 - 100 * comp_size / size);
        }
        printf("\n");
}

static void create_zip(const char *zip_filename, const char *comment,
                       uint16_t n, const char *const *filenames)
{
        time_t mtime;
        time_t *mtimes;
        uint8_t **file_data;
        uint32_t *file_sizes;
        size_t file_size, zip_size;
        uint8_t *zip_data;
        uint16_t i;

        printf("Creating ZIP archive: %s\n\n", zip_filename);

        if (comment != NULL) {
                printf("%s\n\n", comment);
        }

        mtime = time(NULL);

        file_data = xmalloc(sizeof(*file_data) * n);
        file_sizes = xmalloc(sizeof(*file_sizes) * n);
        mtimes = xmalloc(sizeof(*mtimes) * n);

        for (i = 0; i < n; i++) {
                file_data[i] = read_file(filenames[i], &file_size);
                if (file_size >= UINT32_MAX) {
                        printf("%s is too large!\n", filenames[i]);
                        exit(1);
                }
                file_sizes[i] = (uint32_t)file_size;
                mtimes[i] = mtime;
        }

        zip_size = zip_max_size(n, filenames, file_sizes, comment);
        if (zip_size == 0) {
                printf("zip writing not possible");
                exit(1);
        }

        zip_data = xmalloc(zip_size);
        zip_size = zip_write(zip_data, n, filenames,
                             (const uint8_t *const *)file_data,
                             file_sizes, mtimes, comment, zip_callback);

        write_file(zip_filename, zip_data, zip_size);
        printf("\n");

        free(zip_data);
        for (i = 0; i < n; i++) {
                free(file_data[i]);
        }
        free(mtimes);
        free(file_sizes);
        free(file_data);
}

最后,它main检查命令行参数并确定要执行的操作:

static void print_usage(const char *argv0)
{
        printf("Usage:\n\n");
        printf("  %s list <zipfile>\n", argv0);
        printf("  %s extract <zipfile>\n", argv0);
        printf("  %s create <zipfile> [-c <comment>] <files...>\n", argv0);
        printf("\n");
}

int main(int argc, char **argv) {
        printf("\n");
        printf("HWZIP " VERSION " -- A very simple ZIP program ");
        printf("from https://www.hanshq.net/zip.html\n");
        printf("\n");

        if (argc == 3 && strcmp(argv[1], "list") == 0) {
                list_zip(argv[2]);
        } else if (argc == 3 && strcmp(argv[1], "extract") == 0) {
                extract_zip(argv[2]);
        } else if (argc >= 3 && strcmp(argv[1], "create") == 0) {
                if (argc >= 5 && strcmp(argv[3], "-c") == 0) {
                        create_zip(argv[2], argv[4], (uint16_t)(argc - 5),
                                   (const char *const *)&argv[5]);
                } else {
                        create_zip(argv[2], NULL, (uint16_t)(argc - 3),
                                   (const char *const *)&argv[3]);
                }
        } else {
                print_usage(argv[0]);
                return 1;
        }

        return 0;
}

组装说明


完整的源文件集可在hwzip-1.0.zip中获得如何在Linux或Mac上编译HWZip:

$ clang generate_tables.c && ./a.out > tables.c
$ clang -O3 -DNDEBUG -march=native -o hwzip crc32.c deflate.c huffman.c \
        hwzip.c lz77.c tables.c zip.c

在Windows上,在Visual Studio中开发人员的命令提示符下(如果您没有Visual Studio,请下载构建工具):

cl /TC generate_tables.c && generate_tables > tables.c
cl /O2 /DNDEBUG /MT /Fehwzip.exe /TC crc32.c deflate.c huffman.c hwzip.c
        lz77.c tables.c zip.c /link setargv.obj

Setargv.obj用于扩展通配符命令行参数。)

结论


技术的快速和缓慢发展令人惊奇。Zip格式基于30年代和70年代的技术创建于30年前。尽管此后发生了很大的变化,但是Zip文件实际上保持不变,并且今天比以往任何时候都更为普遍。我认为,对它们的工作原理有个很好的了解将很有用。

练习题


  • 使HWZip记录每个文件更改所花费的时间,而不是记录文件创建的当前时间。在Linux或Mac上使用stat(2)在Windows 使用GetFileTime或添加命令行标志,允许用户设置文件更改的特定时间。
  • gzip-. — , Deflate ( ). RFC 1952.
  • Zip- . HWZip , read_file mmap(2) Linux Mac CreateFileMapping Windows.
  • HWZip , Zip64. appnote.txt.



All Articles