Fichiers Zip: historique, explication et implémentation



Je me demande depuis longtemps comment les données sont compressées, y compris dans les fichiers Zip. Une fois, j'ai décidé de satisfaire ma curiosité: apprendre comment fonctionne la compression et écrire mon propre programme Zip. La mise en œuvre est devenue un exercice passionnant de programmation. Vous obtenez un grand plaisir de créer une machine déboguée qui prend les données, transfère ses bits dans une représentation plus efficace, puis les recueille. J'espère que vous serez également intéressé à lire à ce sujet.

L'article explique en détail le fonctionnement des fichiers Zip et du schéma de compression: compression LZ77, algorithme Huffman, algorithme Deflate et plus encore. Vous apprendrez l'histoire du développement de la technologie et examinerez des exemples d'implémentation assez efficaces écrits à partir de zéro en C. Le code source est ici: hwzip-1.0.zip .

Je suis très reconnaissant à Ange Albertini , Gynvael Coldwind , Fabian Giesen , Jonas Skeppstedt ( web ), Primiano Tucci et Nico Weber , qui ont fourni de précieux commentaires sur les versions préliminaires de cet article.

Contenu



RĂ©cit


PKZip


Dans les années 80 et au début des années 90, avant que l'Internet ne se généralise, les passionnés d'informatique utilisaient des modems commutés pour se connecter via le réseau téléphonique au réseau BBS (Bulletin Board Systems). BBS était un système informatique interactif qui permettait aux utilisateurs d'envoyer des messages, de jouer à des jeux et de partager des fichiers. Pour se connecter, il suffisait d'avoir un ordinateur, un modem et un bon numéro de téléphone BBS. Les numéros ont été publiés dans des magazines informatiques et sur d'autres BBS.

L' archiveur était un outil important pour faciliter la distribution des fichiers . Il vous permet d'enregistrer un ou plusieurs fichiers dans un seul fichier d'archivepour stocker ou transmettre plus facilement des informations. Et idéalement, l'archive a également compressé les fichiers pour économiser de l'espace et du temps pour la transmission sur le réseau. À l'époque de BBS, l'archiveur Arc était populaire, écrit par Tom Henderson de System Enhancement Associates (SEA), une petite entreprise qu'il a fondée avec son beau-frère.

À la fin des années 1980, le programmeur Phil Katz a sorti sa propre version d'Arc, PKArc. Il était compatible avec SEA Arc, mais il fonctionnait plus rapidement grâce à des sous-programmes écrits en langage assembleur et utilisait une nouvelle méthode de compression. Le programme est devenu populaire, Katz a quitté son emploi et a créé PKWare pour se concentrer sur son développement. Selon la légende, la plupart des travaux ont eu lieu dans la cuisine de sa mère à Glendale, Wisconsin.


Photo de Phil Katz tirée d'un article du Milwaukee Sentinel du 19 septembre 1994.

Cependant, l'EES n'était pas satisfaite de l'initiative de Katz. L'entreprise l'a accusé de violation de marque et de droit d'auteur. Le litige et la controverse sur le réseau BBS et le monde des PC sont devenus connus sous le nom d' Arc Wars . Finalement, le différend a été réglé en faveur de l'EES.

Abandoning Arc, Katz a créé un nouveau format d'archivage en 1989, qu'il a appelé Zip et mis à la disposition du public :

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

Le programme de Katz pour créer de tels fichiers s'appelait PKZip et s'est rapidement répandu dans le monde du BBS et du PC.

L'un des aspects qui a probablement contribué au succès du format Zip est que la documentation était fournie avec PKZip, Application Note , qui expliquait en détail le fonctionnement du format. Cela a permis à d'autres d'apprendre le format et de créer des programmes qui génèrent, extraient ou interagissent d'une autre manière avec des fichiers Zip.

Zip est un format de compression sans perte : après le déballage, les données seront les mêmes qu'avant la compression. L'algorithme recherche la redondance dans les données source et présente plus efficacement les informations. Cette approche est différente de la compression avec perte., qui est utilisé dans des formats tels que JPEG et MP3: une fois compressées, certaines informations moins visibles à l'œil ou à l'oreille humaine sont rejetées.

PKZip a été distribué sous forme de Shareware: il pouvait être librement utilisé et copié, mais l'auteur a suggéré aux utilisateurs d '«enregistrer» le programme. Pour 47 $, vous pouvez obtenir des instructions imprimées, un support premium et une version améliorée de l'application.


L'une des versions clés de PKZip était la 2.04c, sortie le 28 décembre 1992 (la version 2.04g est sortie peu de temps après ). Il a utilisé l'algorithme de compression Deflate par défaut. La version a déterminé le développement ultérieur de la compression dans les fichiers Zip ( article consacré à la version ).


Depuis lors, le format zip a été utilisé dans de nombreux autres formats de fichiers. Par exemple, les archives Java (.jar), les packages d'application Android (.apk) et les fichiers .docx Microsoft Office utilisent le format Zip. De nombreux formats et protocoles utilisent le même algorithme de compression, Deflate. Disons que les pages Web sont probablement transférées vers votre navigateur sous forme de fichier gzip, dont le format utilise la compression Deflate.

Phil Katz est décédé en 2000. PKWare existe toujours et prend en charge le format Zip, bien que la société se concentre principalement sur les logiciels de protection des données.

Info-zip et zlib


Peu de temps après la sortie de PKZip en 1989, d'autres programmes pour décompresser les fichiers Zip ont commencé à apparaître. Par exemple, un programme de décompression qui pourrait décompresser sur les systèmes Unix. En mars 1990, une liste de diffusion appelée Info-ZIP a été créée.

Le groupe Info-ZIP a publié des programmes open source gratuits unzip et zip , qui ont été utilisés pour décompresser et créer des fichiers zip. Le code a été porté sur de nombreux systèmes, et il est toujours la norme pour les programmes Zip pour les systèmes Unix. Cela a ensuite contribué à accroître la popularité des fichiers Zip.

Une fois que le code Info-ZIP qui a fait la compression et la décompression de dégonflage a été déplacé vers une bibliothèque zlib distincte qu'ils ont écriteJean-loup Gailly (compression) et Mark Adler (déballage).


Jean-loup Gailly (Ă  gauche) et Mark Adler (Ă  droite) au prix USENIX STUG 2009 .

L'une des raisons de la création de la bibliothèque était qu'elle offrait la commodité d'utiliser la compression Deflate dans d'autres applications et formats, par exemple, dans les nouveaux gzip et PNG . Ces nouveaux formats étaient destinés à remplacer Compress et GIF , qui utilisaient l'algorithme LZW protégé par brevet.

Dans le cadre de la création de ces formats, Peter Deutsch a écrit la spécification Deflate et l'a publiée sous le nom Internet RFC 1951 en mai 1996. Cela s'est avéré être une description plus accessible par rapport à la note d'application PKZip d'origine.

Aujourd'hui, zlib est utilisé partout. Peut-être est-il désormais chargé de compresser cette page sur un serveur Web et de la déballer dans votre navigateur. Aujourd'hui, la plupart des fichiers zip sont compressés et décompressés à l'aide de zlib.

Winzip


Beaucoup de ceux qui n'ont pas trouvé PKZip ont utilisé WinZip. Les utilisateurs de PC sont passés de DOS à Windows et de PKZip à WinZip.

Tout a commencé avec un projet du programmeur Nico Mac, qui a créé un logiciel pour OS / 2 au Mansfield Software Group à Storrs-Mansfield, Connecticut. Nico a utilisé le gestionnaire de présentation, il s'agit d'une interface utilisateur graphique sous OS / 2, et il était contrarié de devoir passer d'un gestionnaire de fichiers aux commandes DOS chaque fois qu'il voulait créer des fichiers Zip.

Mac a écrit un programme GUI simple qui fonctionnait avec des fichiers Zip directement dans Presentation Manager, l'a nommé PMZip et l'a publié en tant que shareware dans les années 1990.

OS / 2 n'a pas réussi, et le monde du PC a repris Microsoft Windows. En 1991, Mac a décidé d'apprendre à écrire des programmes Windows, et son premier projet a été de porter son application Zip sur un nouveau système d'exploitation. En avril 1991, WinZip 1.00 est sorti . Il a été distribué sous forme de shareware avec une période d'essai de 21 jours et des frais d'inscription de 29 $. Elle ressemblait à ceci:


Dans les premières versions de WinZip, PKZip était utilisé sous le capot. Mais à partir de la version 5.0 en 1993, le code d'Info-ZIP a commencé à être utilisé pour le traitement direct des fichiers Zip. L'interface utilisateur a également évolué progressivement.


WinZip 6.3 sous Windows 3.11 pour Workgroups.

WinZip était l'un des programmes de shareware les plus populaires dans les années 1990. Mais à la fin, il a perdu sa pertinence en raison de l'intégration de la prise en charge des fichiers Zip dans les systèmes d'exploitation. Windows les utilise comme «dossiers compressés» depuis 2001 (Windows XP), la bibliothèque DynaZip y est utilisée.

Mac s'appelait à l'origine Nico Mak Computing. En 2000, il a été rebaptisé WinZip Computing, et autour de ces années, Mack l'a quitté. En 2005, Vector Capital a vendu la sociétéet, à la fin, elle est devenue la propriété de Corel , qui publie toujours WinZip en tant que produit.

Compression Lempel-Ziv (LZ77)


La compression Zip se compose de deux ingrédients principaux: la compression Lempel-Ziv et le code Huffman.

Une façon de compresser le texte consiste à créer une liste de mots ou d'expressions courantes en remplaçant les variétés de ces mots dans le texte par des liens vers le dictionnaire. Par exemple, le mot long «compression» dans le texte source peut être représenté par # 1234, où 1234 fait référence à la position du mot dans la liste. C'est ce qu'on appelle la compression du dictionnaire .

Mais du point de vue de la compression universelle, cette méthode présente plusieurs inconvénients. Tout d'abord, qu'est-ce qui devrait entrer exactement dans le dictionnaire? Les données source peuvent être dans différentes langues, il peut même ne pas s'agir de texte lisible par l'homme. Et si le dictionnaire n'est pas convenu à l'avance entre la compression et la décompression, il devra alors être stocké et transféré avec les données compressées, ce qui réduit les avantages de la compression.

Une solution élégante à ce problème consiste à utiliser les données source elles-mêmes comme dictionnaire. En 1977, A Universal Algorithm for Sequential Data Compression , Jacob Ziv et Abraham Lempel (qui travaillaient chez Technion), ont proposé un schéma de compression dans lequel les données sources sont présentées comme une séquence de triplets:

(, , )

où ils forment un lien en arrière vers la séquence de caractères que vous souhaitez copier à partir de la position précédente dans le texte d'origine, et c'est le caractère suivant dans les données générées.


Abraham Lempel et Jacob Ziv.

Considérez les lignes suivantes:

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

Dans la deuxième ligne, la séquence «t était le w» peut être représentée comme (26, 10, w), car elle est recréée en copiant 10 caractères de la position de 26 caractères vers la lettre «w». Pour les caractères qui ne sont pas encore apparus, des backlinks de longueur nulle sont utilisés. Par exemple, le «I» initial peut être représenté par (0, 0, I).

Ce schéma est appelé compression Lempel-Ziv ou compression LZ77. Cependant, dans les implémentations pratiques de l'algorithme, une partie du triplet n'est généralement pas utilisée . Au lieu de cela, les caractères sont générés individuellement et les paires ( , ) sont utilisées pour les backlinks (cette option est appelée compression LZSS ). Comment sont encodés littéraux et les backlinks est un problème distinct, nous le considérerons ci-dessous lorsque nous analyserons l'algorithmeLe dégonfler .

Ce texte:

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

Vous pouvez compresser dans ceci:

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

L'une des propriétés importantes des backlinks est qu'ils peuvent se chevaucher. Cela se produit lorsque la longueur est supérieure à la distance. Par exemple:

Fa-la-la-la-la

Vous pouvez compresser pour:

Fa-la(3,9)

Cela peut vous sembler étrange, mais la méthode fonctionne: après avoir copié les octets des trois premiers «-la», la copie continue en utilisant les octets nouvellement générés.

En fait, il s'agit d'un type de codage de longueurs de série , dans lequel une partie des données est copiée à plusieurs reprises pour obtenir la longueur souhaitée.

Un exemple interactif d'utilisation de la compression Lempel-Ziv pour les paroles est présenté dans un article de Colin Morris Les paroles pop deviennent-elles plus répétitives? .

Ce qui suit est un exemple de copie de backlinks en C. Veuillez noter qu'en raison d'un chevauchement possible, nous ne pouvons pas utiliser memcpyou memmove.

/* 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++;
        }
}

Il est facile de générer des littéraux, mais pour être complet, nous utiliserons une fonction auxiliaire:

/* 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;
}

Notez que l'appelant de cette fonction doit s'assurer qu'il y a dstsuffisamment d'espace pour les données générées et que le backlink n'accède pas à la position avant le début de la mémoire tampon.

Il est difficile de ne pas générer de données à l'aide de backlinks lors du déballage, mais de les créer d'abord lors de la compression des données source. Cela peut être fait de différentes manières, mais nous utiliserons la méthode basée sur les tables de hachage de zlib, qui est proposée dans la RFC 1951.

Nous utiliserons une table de hachage avec les positions des préfixes à trois caractères qui ont été trouvés précédemment dans la ligne (les backlinks plus courts n'apportent aucun avantage). Le dégonflage autorise les backlinks dans les 32 768 caractères précédents - cela s'appelle une fenêtre. Cela permet une compression en continu: les données d'entrée sont traitées petit à petit, à condition que la fenêtre contenant les derniers octets soit stockée en mémoire. Cependant, notre implémentation suppose que toutes les données d'entrée sont à notre disposition et que nous pouvons les traiter ensemble à la fois. Cela vous permet de vous concentrer sur la compression plutôt que sur la comptabilité, ce qui est nécessaire pour le traitement des flux.

Nous utiliserons deux tableaux: in headcontient la valeur de hachage du préfixe à trois caractères pour la position dans l'entrée, et in prevcontient la position de la position précédente avec cette valeur de hachage. En fait, head[h]c'est l'en-tête d'une liste chaînée de positions de préfixe avec un hachage h, et prev[x]reçoit l'élément précédant xla liste.

#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;
        }

Pour insérer une nouvelle position de chaîne dans la table de hachage prev, elle est mise à jour pour indiquer la précédente head, puis elle est mise à jour elle head- même :

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;
}

Faites attention à l'opération modulo lors de l'indexation prev: nous ne nous intéressons qu'aux positions qui tombent dans la fenêtre courante.

Au lieu de calculer la valeur de hachage pour chaque préfixe à trois caractères à partir de zéro, nous utiliserons un hachage en anneau et le mettrons constamment à jour afin que seuls les trois derniers caractères soient reflétés dans sa valeur:

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;
}

La carte de hachage peut ensuite être utilisée pour rechercher efficacement les correspondances précédentes avec une séquence, comme indiqué ci-dessous. La recherche de correspondances est l'opération de compression la plus gourmande en ressources, nous limiterons donc la profondeur de la recherche dans la liste.

La modification de divers paramètres, tels que la profondeur de la recherche dans la liste des préfixes et l'exécution de comparaisons paresseuses, comme décrit ci-dessous, est un moyen d'augmenter la vitesse en réduisant le degré de compression. Les paramètres de notre code sont sélectionnés pour correspondre au niveau de compression maximal dans 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;
}

Vous pouvez terminer la fonction avec lz77_compressce code pour rechercher les correspondances précédentes:

       /* 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;
}

Ce code recherche le backlink le plus long qui peut être généré à la position actuelle. Mais avant de le publier, le programme décide s'il est possible de trouver une correspondance encore plus longue dans la position suivante. Dans zlib, cela s'appelle une évaluation de comparaison paresseuse . C'est toujours un algorithme gourmand : il sélectionne la correspondance la plus longue, même si la plus courte actuelle vous permet d'obtenir plus tard une correspondance encore plus longue et d'obtenir une compression plus forte.

La compression Lempel-Ziv peut fonctionner à la fois rapidement et lentement. Zopflia passé beaucoup de temps à rechercher des backlinks optimaux pour obtenir des pourcentages de compression supplémentaires. Ceci est utile pour les données qui sont compressées une fois, puis réutilisées, par exemple, pour des informations statiques sur un serveur Web. De l'autre côté de l'échelle se trouvent des compresseurs tels que Snappy et LZ4 , qui ne sont comparés qu'avec le dernier préfixe de 4 octets et sont très rapides. Ce type de compression est utile dans les bases de données et les systèmes RPC dans lesquels le temps consacré à la compression est rentable en économisant du temps lors de l'envoi de données sur un réseau ou sur disque.

L'idée d'utiliser les données source comme dictionnaire est très élégante, mais vous pouvez également bénéficier d'un dictionnaire statique. Brotli est un algorithme basé sur LZ77, mais il utilise également un granddictionnaire statique de chaînes, que l'on trouve souvent sur le réseau.

Code LZ77 peut être consulté dans lz77.h et lz77.c .

Code Huffman


Le deuxième algorithme de compression Zip est le code Huffman.

Le terme code dans ce contexte est une référence à un système de présentation de données sous une autre forme. Dans ce cas, nous nous intéressons au code qui peut être utilisé pour représenter efficacement les littéraux et les backlinks générés par l'algorithme Lempel-Ziv.

Traditionnellement, le texte anglais est présenté en utilisant l' American Standard Code for Information Interchange (ASCII) . Ce système attribue à chaque caractère un numéro, qui est généralement stocké dans une représentation à 8 bits. Voici les codes ASCII pour les lettres majuscules de l'alphabet anglais:

UNE01000001N01001110
B01000010O01001111
C01000011P01010000
D01000100Q01010001
E01000101R01010010
F01000110S01010011
G01000111T01010100
H01001000U01010101
I01001001V01010110
J01001010W01010111
K01001011X01011000
L01001100Y01011001
M01001101Z01011010

Un octet par caractère est un moyen pratique de stocker du texte. Il vous permet d'accéder facilement ou de modifier des parties du texte, et il est toujours clair combien d'octets sont nécessaires pour stocker N caractères, ou combien de caractères sont stockés dans N octets. Cependant, ce n'est pas le moyen le plus efficace en termes d'espace occupé. Par exemple, en anglais, la lettre E est utilisée le plus souvent et Z est la moins utilisée. Par conséquent, en termes de volume, il est plus efficace d'utiliser une représentation de bits plus courte pour E et une plus longue pour Z, plutôt que d'attribuer le même nombre de bits à chaque caractère.

Un code qui spécifie des codages de différentes longueurs en différents caractères source est appelé un code de longueur variable . L'exemple le plus célèbre est le code Morse., dans lequel chaque caractère est codé avec des points et des tirets, transmis à l'origine par télégraphe avec des impulsions courtes et longues:

UNE• -N- •
B- • • •O- - -
C- • - •P• - - •
ré- • •Q- - • -
E•R• - •
F• • - •S• • •
g- - •T-
H• • • •U• • -
je• •V• • • -
J• - - -W• - -
K- • -X- • • -
L• - • •Oui- • - -
M- -Z- - • •

L'un des inconvénients du code Morse est qu'un mot de code peut être le préfixe d'un autre. Par exemple, • • - • n'a pas de décodage unique: il peut être F ou ER. Ceci est résolu par des pauses (trois points de longueur) entre les lettres pendant la transmission. Cependant, il serait préférable que les mots de code ne puissent pas être des préfixes d'autres mots. Ce code est appelé non préfixé . Le code ASCII d'une longueur fixe n'est pas préfixé car les mots de code ont toujours la même longueur. Mais les codes de longueur variable peuvent également ne pas être préfixés. Les numéros de téléphone ne sont souvent pas préfixés. Avant l'introduction du numéro de téléphone d'urgence 112 en Suède, tous les numéros commençant par 112 devaient être modifiés et aux États-Unis, il n'y a pas un seul numéro de téléphone commençant par 911.

Pour minimiser la taille du message codé, il est préférable d'utiliser un code non préfixé dans lequel les caractères fréquents ont des mots de code plus courts. Le code optimal sera celui qui génère le résultat le plus court possible - la somme des longueurs des mots de code, multipliée par leur fréquence d'occurrence, sera le minimum possible. C'est ce qu'on appelle un code sans préfixe avec une redondance minimale , ou un code Huffman , en l'honneur de l'inventeur d'un algorithme efficace pour générer de tels codes.

Algorithme de Huffman


Tout en étudiant des matériaux pour rédiger sa thèse de doctorat sur l'ingénierie électronique au MIT, David Huffman a suivi un cours sur la théorie de l'information enseigné par Robert Fano. Selon la légende , Fano a permis à ses étudiants de choisir: passer l'examen final ou le cours. Huffman a choisi ce dernier, et il lui a été proposé de rechercher des codes sans préfixe avec une redondance minimale. On suppose qu'il ne savait pas que Fano lui-même travaillait sur cette tâche à ce moment-là (l' algorithme de Shannon-Fano était la méthode la plus célèbre de ces années ). Les travaux de Huffman ont été publiés en 1952 sous le titre A Method for the Construction of Minimum-Redundancy Codes en 1952. Et depuis lors, son algorithme a été largement utilisé.


Communiqué de presse de David Huffman UC Santa Cruz.

L'algorithme de Huffman crée du code non préfixé avec une redondance minimale pour le jeu de caractères et leur fréquence d'utilisation. L'algorithme sélectionne à plusieurs reprises deux caractères les moins susceptibles d'être trouvés dans les données source - disons, X et Y - et les remplace par un caractère composite signifiant «X ou Y». La fréquence d'apparition d'un symbole composite est la somme des fréquences de deux symboles source. Les mots de code pour X et Y peuvent être n'importe quel mot de code affecté au caractère composé «X ou Y» suivi de 0 ou 1 pour distinguer les caractères originaux. Lorsque les données d'entrée sont réduites à un caractère, l'algorithme cesse de fonctionner ( explication vidéo ).

Voici un exemple de l'algorithme travaillant sur un petit jeu de caractères:

symboleLa fréquence
UNE6
B4
C2
ré3

Première itération de traitement:


Les deux symboles les plus rares, C et D, sont supprimés de l'ensemble et remplacés par un symbole composite dont la fréquence est la somme des fréquences C et D:


Maintenant, les symboles les plus rares sont B et un symbole composite avec une fréquence de 5. Ils sont retirés de l'ensemble et remplacés par un symbole composite avec une fréquence de 9:


Enfin, A et un symbole composite avec une fréquence de 9 sont combinés en un nouveau symbole avec une fréquence de 15:


L'ensemble a été réduit à un caractère, le traitement est terminé.

L'algorithme a créé une structure appelée arbre de Huffman . Les caractères saisis sont des feuilles et plus la fréquence d'un caractère est élevée, plus il est situé. À partir de la racine de l'arborescence, vous pouvez générer des mots de code pour les caractères en ajoutant respectivement 0 ou 1 lorsque vous vous déplacez vers la gauche ou la droite. Il se présente comme ceci:

symboleLe mot de code
UNE0
Bdix
C110
ré111

Aucun mot de code n'est un préfixe pour un autre. Plus un symbole apparaît souvent, plus son mot de code est court.

L'arbre peut également être utilisé pour le décodage: on part de la racine et on va à droite ou à gauche pour la valeur avec 0 ou 1 devant le caractère. Par exemple, la ligne 010100 est décodée dans ABBA.

Notez que la longueur de chaque mot de code est équivalente à la profondeur du nœud d'arbre correspondant. Comme nous le verrons dans la partie suivante, nous n'avons pas besoin d'une véritable arborescence pour attribuer des mots de code. Il suffit de connaître la longueur des mots eux-mêmes. Ainsi, le résultat de notre implémentation de l'algorithme de Huffman sera la longueur des mots de code.

Pour stocker le jeu de caractères et trouver efficacement les fréquences les plus basses, nous utiliserons la structure de données du tas binaire . En particulier, nous sommes intéressés parmin-heap , car la valeur minimale doit être en haut.

/* 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);
        }
}

Pour suivre la fréquence des ncaractères, nous utiliserons un tas d' néléments. De plus, chaque fois qu'un symbole composite est créé, nous voulons lui «lier» les deux symboles source. Par conséquent, chaque symbole aura un «élément de communication».

Pour stocker le ntas d' néléments de communication et les éléments de communication, nous utiliserons un tableau d' n * 2 + 1éléments. Lorsque deux caractères du tas sont remplacés par un, nous utiliserons le deuxième élément pour enregistrer le lien vers le nouveau caractère. Cette approche est basée sur la mise en œuvre de la gestion des gigaoctets de Witten, Moffat et Bell.

À chaque nœud du tas, nous utiliserons les 16 bits les plus significatifs pour stocker la fréquence des symboles et les 16 bits les moins significatifs pour stocker l'index de l'élément de communication des symboles. En raison de l'utilisation de bits élevés, la différence de fréquence sera déterminée par le résultat d'une comparaison 32 bits entre deux éléments du tas.

En raison de cette représentation, nous devons nous assurer que la fréquence des caractères tient toujours en 16 bits. Une fois l'algorithme terminé, le symbole composite final aura la fréquence de tous les symboles combinés, c'est-à-dire que cette somme doit être placée sur 16 bits. Notre implémentation Deflate le vérifiera en traitant simultanément jusqu'à 64 535 caractères.

Les symboles de fréquence nulle recevront des mots de code de longueur nulle et ne participeront pas à la compilation du codage.

Si le mot de code atteint la profondeur maximale spécifiée, nous «lisserons» la distribution de fréquence en imposant une limite de fréquence et réessayer (oui, avec l'aide goto). Il existe des moyens plus sophistiqués de faire un codage Huffman limité en profondeur, mais celui-ci est simple et efficace.

#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;
        }
}

Une alternative élégante à l'option de tas binaire consiste à stocker les caractères dans deux files d'attente. Le premier contient les caractères source, triés par fréquence. Lorsqu'un symbole composé est créé, il est ajouté secondairement. Ainsi, le symbole avec la fréquence la plus basse sera toujours en première position de l'une des files d'attente. Cette approche est décrite par Jan van Leeuwen dans On the Construction of Huffman Trees (1976).

Le codage Huffman est optimal pour les codes sans préfixe, mais dans d'autres cas, il existe des méthodes plus efficaces: le codage arithmétique et les systèmes de nombres asymétriques .

Codes canoniques de Huffman


Dans l'exemple ci-dessus, nous avons construit un arbre Huffman:


Si nous partons de la racine et utilisons 0 pour la branche gauche et 1 pour la droite, alors nous obtenons les codes suivants:

symboleLe mot de code
UNE0
Bdix
C110
ré111

La décision d'utiliser 0 pour la branche gauche et 1 pour la branche droite semble arbitraire. Si nous faisons le contraire, nous obtenons:

symboleLe mot de code
UNE1
B01
C001
ré000

Nous pouvons marquer arbitrairement deux branches provenant d'un nœud avec zéro et un (l'essentiel est que les étiquettes sont différentes), et toujours obtenir le code équivalent:


symboleLe mot de code
UNE0
BOnze
C100
ré101

Bien que l'algorithme Huffman fournisse les longueurs de mot de code requises pour le code non préfixé avec une redondance minimale, il existe de nombreuses façons d'attribuer des mots de code individuels.

Étant donné la longueur du mot de code calculé par l'algorithme de Huffman, le code canonique de Huffman attribue les mots de code aux caractères d'une manière spécifique. Ceci est utile car il vous permet de stocker et de transmettre des longueurs de mot de code avec des données compressées: le décodeur pourra récupérer des mots de code en fonction de leur longueur. Bien sûr, vous pouvez stocker et transmettre des fréquences de symboles et exécuter l'algorithme Huffman dans le décodeur, mais cela nécessitera plus de travail et plus de stockage de la part du décodeur. Une autre propriété très importante est que la structure des codes canoniques utilise un décodage efficace.

L'idée est d'affecter les mots de code aux caractères de manière séquentielle, sous un à la fois. Le premier mot de code est 0. Le suivant sera un mot d'une longueur du mot précédent + 1. Le premier mot d'une longueur de N est composé du dernier mot de la longueur N-1, en ajoutant un (pour obtenir un nouveau mot de code) et en décalant d'un pas vers la gauche (pour augmenter la longueur).

Dans la terminologie de l'arbre Hoffman, les mots de code sont affectés séquentiellement aux feuilles dans l'ordre de gauche à droite, un niveau à la fois, se déplaçant vers la gauche lors du passage au niveau suivant.

Dans notre exemple ABCD, l'algorithme Huffman a attribué des mots de code avec des longueurs de 1, 2, 3 et 3. Le premier mot est 0. C'est également le dernier mot de longueur 1. Pour la longueur 2, nous prenons 0 et ajoutons 1 pour obtenir le code suivant, qui deviendra le préfixe des codes à deux bits. , décaler vers la gauche et obtenir 10. C'est maintenant le dernier mot de longueur 2. Pour obtenir la longueur 3, nous ajoutons 1 et décalage: 110. Pour obtenir le mot suivant de longueur 3, nous ajoutons 1: 111.

symboleLe mot de code
UNE0
Bdix
C110
ré111

L'implémentation du générateur de code canonique est illustrée ci-dessous. Notez que l'algorithme Deflate s'attend à ce que les mots de code soient générés sur la base du principe LSB-first (d'abord, le bit le moins significatif). Autrement dit, le premier bit du mot de code doit être stocké dans le bit le moins significatif. Cela signifie que nous devons changer l'ordre des bits, par exemple, en utilisant la table de recherche.

#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);
}

Maintenant, mettez tout cela ensemble et Ă©crivez le code d'initialisation de l'encodeur:

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);
}

Nous faisons également une fonction pour configurer l'encodeur en utilisant les longueurs de code déjà calculées:

/* 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);
}

DĂ©codage efficace de Huffman


Le moyen le plus simple de décoder Huffman est de parcourir l'arborescence à partir de la racine, de lire un bit d'entrée à la fois et de décider quelle branche prendre ensuite, à gauche ou à droite. Lorsqu'un nœud feuille est atteint, il s'agit d'un caractère décodé.

Cette méthode est souvent enseignée dans les universités et les livres. C'est simple et élégant, mais le traitement un bit à la fois est trop lent. Il est beaucoup plus rapide de décoder à l'aide de la table de recherche. Pour l'exemple ci-dessus, dans lequel la longueur maximale du mot de code est de trois bits, vous pouvez utiliser le tableau suivant:

MorceauxsymboleLongueur du mot de code
0 00UNE1
0 01UNE1
0 10UNE1
0 11UNE1
10 0B2
10 1B2
110C3
111ré3

Bien qu'il n'y ait que quatre caractères, nous avons besoin d'un tableau avec huit entrées pour couvrir toutes les combinaisons possibles de trois bits. Les symboles dont les mots de code sont inférieurs à trois bits ont plusieurs entrées dans le tableau. Par exemple, le mot 10 a été «complété» par 10 0 et 10 1 pour couvrir toutes les combinaisons de trois bits commençant par 10.

Pour décoder de cette façon, vous devez indexer dans le tableau avec les trois bits d'entrée suivants et trouver immédiatement le caractère correspondant et la longueur de son mot de code. La longueur est importante, car malgré la recherche des trois bits suivants, nous devons obtenir le même nombre de bits d'entrée que la longueur du mot de code.

La méthode basée sur la table de recherche fonctionne très rapidement, mais elle a un inconvénient: la taille de la table double avec chaque bit supplémentaire dans la longueur du mot de code. Autrement dit, la construction de la table ralentit de façon exponentielle, et si elle cesse de tenir dans le cache du processeur, alors la méthode commence à fonctionner lentement.

Pour cette raison, la table de recherche est généralement utilisée uniquement pour les mots de code ne dépassant pas une certaine longueur. Et pour les mots plus longs, adoptez une approche différente. Tout comme le codage Huffman attribue des mots de code plus courts à des caractères plus fréquents, l'utilisation d'une table de recherche pour les mots de code courts est dans de nombreux cas une excellente optimisation.

Dans zlibplusieurs niveaux de tables de recherche sont utilisés. Si le mot de code est trop long pour la première table, la recherche ira à la table secondaire pour indexer les bits restants.

Mais il existe une autre méthode, très élégante, basée sur les propriétés des codes canoniques de Huffman. Il est décrit dans On the Implementation of Minimum Redundancy Prefix Codes (Moffat et Turpin, 1997) et est également expliqué dans The Lost Huffman Paper de Charles Bloom.

Prenons les mots de code de la version canonique: 0, 10, 110, 111. Nous allons suivre les premiers mots de code de chaque longueur, ainsi que le nombre de chaque mot de code dans la séquence générale - «index des symboles».

Longueur du mot de codePremier mot de codeIndex du premier caractère
101 (A)
2dix2 (B)
31103 (C)

Puisque les mots de code sont attribués séquentiellement, si nous connaissons le nombre de bits, nous pouvons trouver dans le tableau ci-dessus le caractère que ces bits représentent. Par exemple, pour trois bits 111, nous voyons qu'il s'agit d'un décalage d'un par rapport au premier mot de code de cette longueur (110). Le premier index des caractères de cette longueur est 3, et un décalage de un nous donne un index de 4. Un autre tableau compare l'index des caractères avec le caractère:

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

Une petite optimisation: au lieu de stocker séparément le premier index de caractères et le premier mot de code, nous pouvons stocker le premier index moins le premier mot de code dans le tableau:

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

Pour comprendre combien de bits doivent être estimés, nous utilisons à nouveau la propriété de séquence de code. Dans notre exemple, tous les mots de code à un bit valides sont strictement inférieurs à 1, deux bits - strictement inférieurs à 11, trois bits - inférieurs à 1000 (en fait, vrai pour toutes les valeurs à trois bits). En d'autres termes, le mot de code N bits valide doit être strictement inférieur au premier mot de code N bits plus le nombre de mots de code N bits. De plus, nous pouvons déplacer ces limites vers la gauche afin qu'elles soient toutes larges de trois bits. Appelons cela des bits restrictifs pour chacune des longueurs de mot de code:

Longueur du mot de codeBits limites
1100
2110
31000

Le limiteur pour la longueur 3 a dépassé 4 bits, mais cela signifie seulement que n'importe quel mot de trois bits fera l'affaire.

Nous pouvons rechercher parmi les données d'entrée à trois bits et comparer avec les bits restrictifs pour comprendre la longueur de notre mot de code. Après l'achèvement, nous décalons les bits d'entrée, uniquement pour calculer leur nombre correct, puis trouvons l'index des caractères:

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;
        }
}

La complexité temporelle du processus est linéaire par rapport au nombre de bits dans les mots de code, mais la place est dépensée efficacement, seul le chargement et la comparaison sont requis à chaque étape, et comme les mots de code plus courts sont plus courants, la méthode permet d'optimiser la compression dans de nombreuses situations.

Code décodeur complet:

#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;
}

Pour configurer le décodeur, nous pré-calculerons les codes canoniques, comme pour huffman_encoder_init , et remplirons différents tableaux:

/* 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.");
        }
}

DĂ©gonfler


L'algorithme Deflate, introduit dans PKZip 2.04c en 1993, est une méthode de compression standard dans les fichiers Zip modernes. Il est également utilisé en gzip, PNG et de nombreux autres formats. Il utilise une combinaison de compression LZ77 et de codage Huffman, que nous discuterons et implémenterons dans cette section.

Avant Deflate, PKZip utilisait les méthodes de compression Shrink, Reduce et Implode. Aujourd'hui, ils sont rares, bien qu'après Deflate étaient encore utilisés pendant un certain temps, car ils consommaient moins de mémoire. Mais nous ne les considérerons pas.

Flux binaires


Deflate stocke les mots de code Huffman dans un flux binaire selon le principe LSB-first. Cela signifie que le premier bit du flux est stocké dans le bit le moins significatif du premier octet.

Considérons un train de bits (lu de gauche à droite) 1-0-0-1-1. Lorsqu'il est stocké selon le principe LSB-first, la valeur d'octet devient 0b00011001 (binaire) ou 0x19 (hexadécimal). Il peut sembler que le flux est simplement représenté en arrière (dans un sens, il l'est), mais l'avantage est qu'il nous est plus facile d'obtenir les N premiers bits d'un mot d'ordinateur: nous masquons simplement les N bits les moins significatifs.

Ces procédures sont extraites de 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;
}

Notre décodeur Huffman doit regarder les bits suivants dans le flux (suffisamment de bits pour le mot de code le plus long possible), puis continuer le flux par le nombre de bits utilisés par le symbole décodé:

#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;
}

L'essentiel est que sur les machines 64 bits, istream_bitsvous pouvez généralement exécuter en tant qu'instruction de démarrage unique et un peu d'arithmétique, étant donné que les éléments de structure istream_tsont dans des registres. read64le est implémenté dans bits.h (les compilateurs modernes le convertissent en un seul téléchargement 64 bits en utilisant le principe little-endian):

/* 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);
}

Nous avons également besoin d'une fonction pour continuer le flux binaire jusqu'à la frontière de l'octet suivant:

/* 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;
}

Pour un train de bits sortant, nous écrivons des bits en utilisant un processus lecture-modification-écriture. Dans le cas rapide, vous pouvez écrire un peu en utilisant une lecture 64 bits, une sorte d'opération bit et une écriture 64 bits.

/* 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);
}

Nous devons également écrire efficacement des octets dans le flux. Bien sûr, vous pouvez exécuter à plusieurs reprises des enregistrements 8 bits, mais ce sera beaucoup plus rapide à utiliser 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;
}

DĂ©ballage (inflation)


Puisque l'algorithme de compression est appelé Dégonfler - souffler, extraire l'air de quelque chose - le processus de déballage est parfois appelé Inflation . Si vous étudiez d'abord ce processus, nous comprendrons comment fonctionne le format. Vous pouvez voir le code dans la première partie de deflate.h et deflate.c , bits.h , tables.h et tables.c (généré à l'aide de generate_tables.c ).

Les données compressées à l'aide de Deflate sont stockées sous la forme d'une série de blocs.. Chaque bloc commence par un en-tête de 3 bits, dans lequel le premier bit (le moins significatif) est défini s'il s'agit du dernier bloc de la série, et les deux autres bits indiquent son type.


Il existe trois types de blocs: non compressés (0), compressés à l'aide de codes Huffman fixes (1) et compressés à l'aide de codes Huffman «dynamiques» (2).

Ce code effectue le décompactage en utilisant des fonctions auxiliaires pour différents types de blocs, que nous implémenterons plus tard:

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;
}

Blocs de dégonflage non compressés


Ce sont des blocs "stockés", le type le plus simple. Il commence par la limite suivante de 8 bits du flux binaire avec un mot de 16 bits (len) indiquant la longueur du bloc. Derrière, il y a un autre mot de 16 bits (nlen), qui complète (l'ordre des bits est inversé) des mots len. On suppose que nlen agit comme une simple somme de contrôle len: si le fichier est endommagé, les valeurs ne seront probablement pas complémentaires et le programme pourra détecter une erreur.


Après len et nlen sont des données non compressées. Étant donné que la longueur de bloc est une valeur de 16 bits, la taille des données est limitée à 65 535 octets.

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;
}

DĂ©gonfler les blocs en utilisant des codes Huffman fixes


Les blocs Deflate compressés utilisent le code Huffman pour représenter une séquence de littéraux LZ77. Les backlinks sont cassés à l'aide de marqueurs de fin de bloc. Pour les littéraux, les longueurs de backlink et les marqueurs, le code litlen Huffman est utilisé . Et pour les distances de backlink, le code dist est utilisé .


Litlen code les valeurs dans la plage 0-285. Les valeurs 0-255 sont utilisées pour les octets littéraux, 256 est le marqueur de fin de bloc et 257-285 sont utilisées pour les longueurs de liaison retour.

Les backlinks ont une longueur de 3 à 258 octets. La valeur Litlen détermine la longueur de base à laquelle zéro ou plusieurs bits supplémentaires sont ajoutés à partir du flux pour obtenir la longueur totale conformément au tableau ci-dessous. Par exemple, une valeur litlen de 269 signifie une longueur de base de 19 et deux bits supplémentaires. L'ajout de deux bits du flux donne la longueur finale de 19 à 22.

LitlenBits supplémentairesLongueurs
25703
25804
25905
26006
26107
26208
26309
2640dix
265111-12
266113-14
267115-16
268117-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

Veuillez noter qu'une valeur litlen de 284 plus 5 bits supplémentaires peut représenter des longueurs de 227 à 258, cependant, la spécification stipule que la longueur 258 - la longueur maximale du backlink - doit être représentée en utilisant une valeur litlen distincte. Ceci est censé réduire le codage dans les situations où la longueur maximale est souvent rencontrée.

Le décompresseur utilise la table pour obtenir la longueur de base et les bits supplémentaires de la valeur litlen (moins 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 }
};

Le code litlen fixe de Huffman est canonique et utilise les longueurs de mot de code suivantes (286–287 ne sont pas des valeurs litlen valides, mais elles sont impliquées dans la génération de code):

Valeurs de LitlenLongueur du mot de code
0–1438
144–2559
256–2797
280–2878

Le décompresseur stocke ces longueurs dans une table pratique pour la transmission à huffman_decoder_init:

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

...

/* 287 */ 8,
};

Les distances de liaison retour varient de 1 à 32 768. Elles sont codées à l'aide d'un schéma similaire à un schéma de codage de longueur. Le code Huffman dist code des valeurs de 0 à 29, chacune correspondant à la longueur de base, à laquelle des bits supplémentaires sont ajoutés pour obtenir la distance finale:

DistBits supplémentairesDistances
001
102
203
304
415-6
517-8
629-12
7213-16
8317-24
9325–32
dix433–48
Onze449–64
12565–96
treize597-128
146129–192
quinze6193–256
seize7257–384
177385-512
dix-huit8513-768
dix-neuf8769-1024
vingt91025-1536
2191537-2048
22dix2049-3072
23dix3073–4096
24Onze4097-6144
25Onze6145–8192
26128193–12288
271212289–16384
28treize16385–24576
29treize24577–32768

Le code fixe dist de Huffman est canonique. Tous les mots de code ont une longueur de 5 bits. C'est simple, le décompresseur stocke les codes dans une table qui peut être utilisée avec huffman_decoder_init(les valeurs dist 30–31 ne sont pas correctes. Il est indiqué qu'elles sont impliquées dans la génération des codes Huffman, mais n'ont en fait aucun effet):

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

...

/* 31 */ 5,
};

Code de décompression ou de déballage - Dégonflez le bloc en utilisant des codes Huffman fixes:

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;
        }
}

Faites attention à cette optimisation: lorsqu'il n'y a pas assez d'espace dans le tampon sortant, nous émettons des backlinks en utilisant la fonction ci-dessous, qui copie 64 bits à la fois. C'est "désordonné" dans le sens où il copie souvent quelques octets supplémentaires (jusqu'au multiple de 8 suivant). Mais cela fonctionne beaucoup plus rapidement lz77_output_backref, car il nécessite moins d'itérations cycliques et d'accès à la mémoire. En fait, les backlinks courts seront désormais traités en une seule itération, ce qui est très bon pour prédire la ramification.

/* 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);
}

DĂ©gonfler les blocs Ă  l'aide de codes Huffman dynamiques


Dégonfler les blocs à l'aide de codes Huffman dynamiques fonctionne de la même manière que décrit ci-dessus. Mais au lieu des codes prédéfinis pour litlen et dist, ils utilisent les codes stockés dans le flux Deflate lui-même au début du bloc. Le nom est probablement infructueux, car les codes Huffman dynamiques sont également appelés codes qui changent pendant le codage - c'est le codage Huffman adaptatif . Les codes décrits ici n'ont rien à voir avec cette procédure. Ils ne sont dynamiques que dans le sens où différents blocs peuvent utiliser des codes différents.

La génération de codes litlen et dist dynamiques est la partie la plus difficile du format Deflate. Mais dès que les codes sont générés, la décompression est effectuée de la même manière que celle décrite dans la partie précédente, en utilisant 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);
}

Les codes Litlen et dist pour les blocs Deflate dynamiques sont stockés sous la forme d'une série de longueurs de mots de code. Les longueurs elles-mêmes sont codées à l'aide du troisième code Huffman - codelen . Ce code est déterminé par la longueur des mots de code ( codelen_lens) qui sont stockés dans le bloc (ai-je mentionné que c'était difficile?).


Au début du bloc dynamique, il y a 14 bits qui déterminent le nombre de litlen-, dist et codelen longueurs de mots de code qui doivent être lus dans le bloc:

#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;
        }

Viennent ensuite les longueurs de mot de code pour le code codelen. Ces longueurs sont les valeurs habituelles à trois bits, mais écrites dans l'ordre spécial spécifié dans codelen_lengths_order. Puisque vous devez déterminer 19 longueurs, seules seront lues dans le flux num_codelen_lens; tout le reste est implicitement nul. Les longueurs sont répertoriées dans un certain ordre afin que les longueurs nulles soient plus susceptibles de tomber à la fin de la liste et de ne pas être stockées dans le bloc.

       /* 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;
        }

En réglant le décodeur codelen, nous pouvons lire les longueurs des mots de code litlen et dist du flux.

       /* 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 et 18 ne sont pas des longueurs réelles, ce sont des indicateurs que la longueur précédente doit être répétée plusieurs fois, ou que vous devez répéter la longueur zéro:

               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;
                }
        }

Notez que les longueurs litlen et dist sont lues une par une dans le tableau code_lengths. Ils ne peuvent pas être lus séparément, car les longueurs de code peuvent être reportées des dernières longueurs litlen aux premières longueurs dist.

Après avoir préparé les longueurs des mots de code, nous pouvons configurer les décodeurs Huffman et revenir à la tâche de décodage des littéraux et des backlinks:

       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;
}

Compression (déflation)


Dans les parties précédentes, nous avons créé tous les outils nécessaires à la compression Deflate: Lempel-Ziv, le codage Huffman, les flux binaires et une description des trois types de blocs Deflate. Et dans cette partie, nous allons tout rassembler pour obtenir la compression Deflate.

Compression Lempel-Ziv analyse les données source dans une séquence de backlinks et de littéraux. Cette séquence doit être divisée et codée en blocs Deflate, comme décrit dans la partie précédente. Le choix d'une méthode de partitionnement est souvent appelé blocage.. D'une part, chaque nouveau bloc signifie une sorte de surcharge, dont le volume dépend du type de bloc et de son contenu. Moins de blocs - moins de frais généraux. D'un autre côté, ces coûts de création d'un nouveau bloc peuvent être payants. Par exemple, si les caractéristiques des données vous permettent d'effectuer plus efficacement le codage Huffman et de réduire la quantité totale de données générées.

Le blocage est une tâche d'optimisation difficile. Certains compresseurs (par exemple, Zopfli ) essaient mieux que d'autres, mais la plupart utilisent simplement l'approche gourmande: ils émettent des blocs dès qu'ils atteignent une certaine taille.

Différents types de blocs ont leurs propres restrictions de taille:

  • Les blocs non compressĂ©s ne peuvent pas contenir plus de 65 535 octets.
  • Les codes fixes Huffman n'ont pas de taille maximale.
  • Les codes Huffman dynamiques n'ont gĂ©nĂ©ralement pas de taille maximale, mais comme notre implĂ©mentation de l'algorithme Huffman utilise des sĂ©quences de caractères 16 bits, nous sommes limitĂ©s Ă  65 535 caractères.

Pour utiliser librement des blocs de tout type, limitez leur taille Ă  65 534 octets:

/* 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

Pour suivre le flux binaire sortant et le contenu du bloc actuel pendant la compression, nous utiliserons la structure:

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));
}

Pour ajouter les résultats du travail au bloc, lz77_compressnous utiliserons les fonctions de rappel, et une fois la taille maximale atteinte, nous écrirons le bloc dans le flux binaire:

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;
}

La chose la plus intéressante est l'enregistrement de blocs. Si le bloc n'est pas compressé, alors tout est simple:

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;
}

Pour écrire un bloc Huffman statique, nous générons d'abord des codes canoniques basés sur des longueurs de mot de code fixes pour les codes litlen et dist. Ensuite, nous itérons le bloc, notant les caractères qui utilisent ces codes:

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;
}

Bien sûr, écrire des blocs dynamiques Huffman est plus difficile car ils contiennent un codage délicat des codes litlen et dist. Pour représenter cet encodage, nous utilisons la structure suivante:

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

Tout d'abord, nous supprimons la queue de leurs longueurs nulles des mots de code litlen et dist, puis les copions dans un tableau normal pour le codage ultérieur. Nous ne pouvons pas supprimer tous les zéros: il est impossible de coder un bloc Deflate s'il ne contient pas un seul code dist. Il est également impossible d'avoir moins de 257 codes litlen, mais comme nous avons toujours un marqueur de fin d'octet, il y aura toujours une longueur de code non nulle pour un caractère de 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);
}

Après avoir ajouté les longueurs de code dans un tableau, nous effectuons le codage en utilisant des caractères spéciaux pour exécuter les mêmes longueurs de code.

/* 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;
}

Les caractères utilisés pour l'encodage seront enregistrés en utilisant le code Huffman - codelen. Les longueurs des mots de code du code codelen sont écrites dans le bloc dans un ordre spécifique afin que les longueurs nulles soient plus susceptibles de se retrouver à la fin. Voici une fonction qui calcule le nombre de longueurs à écrire:

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;
}

Supposons que nous ayons déjà défini des codes litlen et dist, mis en place un codage des longueurs de leurs mots de code et un code pour ces longueurs. Maintenant, nous pouvons écrire un bloc Huffman dynamique:

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);
}

Pour chaque bloc, nous voulons utiliser le type qui nécessite le moins de bits. La longueur d'un bloc non compressé peut être calculée rapidement:

/* 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;
}

Pour les blocs codés Huffman, vous pouvez calculer la longueur du corps à l'aide des fréquences litlen- et dist des caractères et des longueurs de mot de code:

/* 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;
}

La longueur totale du bloc statique est de 3 bits de l'en-tête plus la longueur du corps. Le calcul de la taille de l'en-tête d'un bloc dynamique nécessite beaucoup plus de travail:

/* 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);
}

Maintenant, nous allons tout assembler et créer la fonction principale pour écrire des blocs:

/* 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;
}

Enfin, l'initiateur de l'ensemble du processus de compression doit définir l'état initial, démarrer la compression Lempel-Ziv et écrire le bloc résultant:

/* 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;
}

Format de fichier Zip


Ci-dessus, nous avons examiné le fonctionnement de la compression Deflate utilisée dans les fichiers Zip. Qu'en est-il du format de fichier lui-même? Dans cette partie, nous examinerons en détail sa structure et sa mise en œuvre. Le code est disponible en zip.h et zip.c .

Aperçu


Le format de fichier est décrit dans la note d'application PKZip :

  1. Chaque fichier, ou élément d'archive, dans un fichier zip a un en- tête de fichier local avec des métadonnées sur l'élément.
  2. . , , , Zip-.
  3. , . , . Zip-.


Chaque élément d'archive est compressé et stocké individuellement. Cela signifie que même s'il existe des correspondances entre les fichiers de l'archive, ils ne seront pas pris en compte pour améliorer la compression.

L'emplacement du catalogue central à la fin vous permet de compléter progressivement l'archive. Lorsque les éléments du fichier sont compressés, ils sont ajoutés à l'archive. L'index est enregistré après toutes les tailles compressées, ce qui vous permet de connaître les décalages de tous les fichiers. L'ajout de fichiers à une archive existante est assez facile, il est placé après le dernier élément et le répertoire central est écrasé.

La possibilité de créer progressivement des archives était particulièrement importante pour diffuser des informations sur de nombreuses disquettes ou volumes. Pendant la compression, PKZip a suggéré aux utilisateurs d'insérer de nouvelles disquettes et d'écrire le répertoire central dans les derniers (derniers). Pour décompresser une archive multi-volumes, PKZip a d'abord demandé que la dernière disquette écrive pour lire le répertoire central, puis le reste de la disquette nécessaire pour extraire les fichiers demandés.

Cela peut vous surprendre, mais aucune règle n'interdit d'avoir plusieurs fichiers portant le même nom dans l'archive. Cela pourrait entraîner beaucoup de confusion lors du déballage: s'il y a plusieurs fichiers avec le même nom, alors lequel devriez-vous décompresser? À son tour, cela pourrait entraîner des problèmes de sécurité. En raison du bogue «Master Key» dans Android (CVE-2013-4787 , diapositives et vidéo du rapport sur Black Hat), un attaquant pourrait contourner les vérifications du système d'exploitation de signature cryptographique lors de l'installation des programmes. Les programmes Android sont distribués dans des fichiers APK , qui sont des fichiers Zip. Il s'est avéré que si l'APK contenait plusieurs fichiers du même nom, le code de vérification de signature a sélectionné le dernier fichier du même nom et le programme d'installation a sélectionné le premier fichier, c'est-à-dire que la vérification n'a pas été effectuée. En d'autres termes, cette petite différence entre les deux bibliothèques Zip a permis de contourner l'ensemble du modèle de sécurité du système d'exploitation.

Contrairement à la plupart des formats, les fichiers zip ne doivent pas commencer par une signature ou un nombre magique. Il n'est généralement pas indiqué que le fichier Zip doit démarrer d'une manière spécifique, ce qui vous permet facilement de créer des fichiers qui sont à la fois valides en tant que Zip et dans un format différent - les fichiers polyglottes . Par exemple, les archives Zip auto-extractibles (par exemple, pkz204g.exe ) sont généralement à la fois des fichiers exécutables et Zip: la première partie est exécutable, suivie d'un fichier Zip (qui est décompressé par la partie exécutable). Le système d'exploitation peut l'exécuter en tant que fichier exécutable, mais le programme Zip l'ouvrira en tant que fichier Zip. Cette fonctionnalité peut vous empêcher de demander une signature au début du fichier.

Bien que ces fichiers polyglottes soient intelligents, ils peuvent entraîner des problèmes de sécurité car ils peuvent tromper les programmes qui tentent de déterminer le contenu du fichier et permettent également de diffuser du code malveillant dans un endroit contenant des fichiers de différents types. Par exemple, les exploits utilisaient des fichiers GIFAR , qui sont en même temps des images GIF et des archives Java correctes (JAR, un type de fichier Zip). Voir l'article sur les formats de fichiers abusifs (à partir de la page 18) pour en savoir plus sur ce problème .

Comme nous le verrons ci-dessous, les fichiers Zip utilisent des champs 32 bits pour les décalages et les tailles afin de limiter la taille de l'archive et de ses éléments à quatre gigaoctets. Dans la note d'application 4.5PKWare a ajouté des extensions de format permettant l'utilisation de décalages et de tailles 64 bits. Les fichiers utilisant ces extensions sont au format Zip64, mais nous ne les considérerons pas.

Structures de données


Fin de l'entrée du répertoire central


La fin d'une entrée de répertoire central (EOCDR) est généralement utilisée comme point de départ pour la lecture d'un fichier zip. Il contient l'emplacement et la taille du répertoire central, ainsi que des commentaires facultatifs sur l'archive entière.

Dans les fichiers Zip qui occupaient plusieurs disquettes - ou volumes - EOCDR contenait également des informations sur le disque que nous utilisons actuellement, sur quel disque le répertoire central démarre, etc. Aujourd'hui, cette fonctionnalité est rarement utilisée et le code de cet article ne traite pas ces fichiers.

L'EOCDR est déterminé par la signature «P» «K», suivie des octets 5 et 6. Elle est suivie de la structure ci-dessous, les nombres sont stockés selon le principe du petit-boutien:

/* 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. */
};

L'EOCDR doit se trouver à la fin du fichier. Mais comme il peut y avoir un commentaire de longueur arbitraire de 16 bits dans sa queue, il peut être nécessaire de trouver une position spécifique:

/* 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;
}

L'enregistrement d'EOCDR est facile. Cette fonction Ă©crit et retourne le nombre d'octets Ă©crits:

/* 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);
}

En-tĂŞte de fichier central


Le répertoire central se compose d'en-têtes de fichiers centraux écrits l'un après l'autre, un pour chaque élément d'archive. Chaque en-tête commence par la signature «P», «K», 1, 2, puis il existe une telle structure:

#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_veret extract_verencoder des informations sur le système d'exploitation et la version du programme utilisé pour ajouter cet élément, ainsi que la version nécessaire pour le récupérer. Les huit bits les plus importants codent le système d'exploitation (par exemple, 0 signifie DOS, 3 signifie Unix, 10 signifie Windows NTFS) et les huit bits inférieurs codent la version du logiciel. Définissez la valeur décimale sur 20, ce qui signifie la compatibilité avec PKZip 2.0.

gp_flagcontient différents drapeaux. Nous sommes intéressés:

  • Bit 0, indiquant le fait du chiffrement de l'Ă©lĂ©ment (nous ne considĂ©rerons pas cela);
  • Et les bits 1 et 2, codant le niveau de compression de dĂ©gonflage (0 - normal, 1 - maximum, 2 - rapide, 3 - très rapide).

methodcode une méthode de compression. 0 - données non compressées, 8 - suppression appliquée. D'autres valeurs concernent des algorithmes anciens ou nouveaux, mais presque tous les Zip utilisent ces deux valeurs.

mod_timeet mod_datecontiennent la date et l'heure de modification du fichier, encodées au format MS-DOS . En utilisant ce code, nous convertirons les horodatages C habituels time_tvers et depuis le format 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. */
}

Le champ crc32contient la valeur du code cyclique redondant des données non compressées. Il est utilisé pour vérifier l'intégrité des données après la récupération. Implémentation ici: crc32.c .

comp_sizeet uncomp_sizecontiennent la taille compressée et non compressée des données du fichier d'élément. Les trois champs suivants contiennent la longueur du nom, du commentaire et des données supplémentaires immédiatement après le titre. disk_nbr_startConçu pour les archives utilisant plusieurs disquettes.

int_attrset ext_attrsdécrire les attributs internes et externes du fichier. Les internes concernent le contenu du fichier, par exemple, le bit le moins significatif indique si le fichier contient uniquement du texte. Les attributs externes indiquent si le fichier est masqué, en lecture seule, etc. Le contenu de ces champs dépend de l'OS, en particulier, demade_by_ver. Sous DOS, les 8 bits inférieurs contiennent l'octet d'attribut de fichier, qui peut être obtenu à partir de l'appel système Int 21 / AX = 4300h . Par exemple, le bit 4 signifie qu'il s'agit d'un répertoire et le bit 5 signifie que l'attribut «archive» est défini (vrai pour la plupart des fichiers sous DOS). Pour autant que je comprends, pour des raisons de compatibilité, ces bits sont définis de manière similaire dans d'autres systèmes d'exploitation. Sous Unix, les 16 bits de poids fort de ce champ contiennent des bits en mode fichier qui sont renvoyés par stat (2) dans st_mode.

lfh_offsetnous indique où chercher l'en-tête du fichier local. name- nom de fichier (ligne C), et comment- commentaire facultatif pour cet élément d'archive (ligne C). extrapeut contenir des données supplémentaires facultatives telles que des informations sur le propriétaire du fichier Unix, une date et une heure plus précises de la modification ou des champs Zip64.

Cette fonction permet de lire les en-tĂŞtes centraux des fichiers:

/* 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);
}

En-tĂŞte de fichier local


Les données de chaque élément d'archive sont précédées d'un en-tête de fichier local, qui répète la plupart des informations de l'en-tête central.

Il est probable que la duplication des données dans les en-têtes centraux et locaux ait été introduite afin que PKZip ne conserve pas l'intégralité du répertoire central en mémoire lors du déballage. Au lieu de cela, à mesure que chaque fichier est extrait, son nom et d'autres informations peuvent être lus à partir de l'en-tête local. De plus, les en-têtes locaux sont utiles pour récupérer des fichiers à partir d'archives Zip dans lesquelles le répertoire central est manquant ou endommagé.

Cependant, cette redondance est également la principale source d'incertitude. Par exemple, que se passe-t-il si les noms de fichier dans les en-têtes centraux et locaux ne correspondent pas? Cela entraîne souvent des bugs et des problèmes de sécurité.

Toutes les informations de l'en-tête central ne sont pas dupliquées. Par exemple, des champs avec des attributs de fichier. De plus, si le troisième bit le moins significatif gp_flags(CRC-32) est spécifié , les champs compressés et non compressés seront réinitialisés à zéro, et ces informations peuvent être trouvées dans le bloc Data Descriptor après les données du fichier lui-même (nous ne les prendrons pas en compte). Cela vous permet d'enregistrer un en-tête local avant que la taille de fichier de l'élément ne soit connue ou à quelle taille il sera compressé.

L'en-tête local commence par la signature «P», «K», 3, 4, puis il existe une telle structure:

/* 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;
};

Ces fonctions lisent et écrivent des en-têtes locaux, comme d'autres structures de données:

/* 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);
}

Implémentation de Zip Read


En utilisant les fonctions ci-dessus, nous implémentons la lecture du fichier Zip en mémoire et obtenons un itérateur pour accéder aux éléments d'archive:

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;
}

Comme je l'ai mentionné ci-dessus, les itérateurs d'élément sont simplement des décalages vers l'en-tête du fichier central à travers lesquels vous pouvez accéder aux données d'élément:

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;
}

Implémentation des enregistrements Zip


Pour écrire un fichier zip dans la mémoire tampon, vous devez d'abord déterminer la quantité de mémoire à lui allouer. Et comme nous ne savons pas combien de données nous allons compresser avant d'essayer d'écrire, nous calculons la limite supérieure en fonction de la taille des éléments non compressés:

/* 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;
}

Ce code écrit le fichier zip en utilisant la compression de dégonflage de chaque élément, en réduisant leur taille:

/* 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);
}

Hwzip


Maintenant, nous savons lire et écrire des fichiers Zip, comment compresser et décompresser les données qui y sont stockées. Écrivons maintenant un simple programme Zip contenant tous ces outils. Le code est disponible sur hwzip.c .

Nous utiliserons une macro pour une gestion simple des erreurs et plusieurs fonctions auxiliaires pour l'allocation de mémoire vérifiée:

#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;
}

Les deux autres fonctions sont utilisées pour lire et écrire des fichiers:

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");
}

Notre programme Zip peut remplir trois fonctions: faire une liste du contenu des fichiers Zip et l'extraire, ainsi que créer des fichiers Zip. L'inscription n'est nulle part plus simple:

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);
}

L'extraction est un peu plus compliquée. Nous utiliserons des fonctions auxiliaires pour la terminaison nulle du nom de fichier (pour le transmettre fopen) et le déballage:

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;
}

Notre programme ignorera tous les éléments d'archive qui ont des répertoires. Ceci est fait afin d'éviter les attaques dites de traversée de chemin : une archive malveillante est utilisée pour écrire le fichier depuis l'extérieur du répertoire spécifié par l'utilisateur. Lisez les détails dans la FAQ Info-ZIP .

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);
}

Pour créer une archive zip, nous allons lire les fichiers d'entrée et les nourrir zip_write. Étant donné que la bibliothèque C standard ne vous permet pas d'obtenir l'heure de modification du fichier, nous utiliserons l'heure actuelle (je laisse cela comme un devoir pour corriger cette fonctionnalité).

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);
}

Enfin, il mainvérifie les arguments de la ligne de commande et décide quoi faire:

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;
}

instructions de montage


Un ensemble complet de fichiers source est disponible sur hwzip-1.0.zip . Comment compiler HWZip sous Linux ou Mac:

$ 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

Sous Windows, à l'invite de commandes du développeur dans Visual Studio (si vous ne disposez pas de Visual Studio, téléchargez les outils de génération ):

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 pour développer les arguments de ligne de commande génériques .)

Conclusion


Il est étonnant de voir comment la technologie évolue rapidement et lentement. Le format Zip a été créé il y a 30 ans sur la base de la technologie des années 50 et 70. Et bien que beaucoup de choses aient changé depuis lors, les fichiers Zip, en fait, sont restés les mêmes et sont aujourd'hui plus courants que jamais. Je pense qu'il sera utile d'avoir une bonne compréhension de leur fonctionnement.

Des exercices


  • Faites en sorte que HWZip enregistre l'heure de modification du fichier et non l'heure de crĂ©ation de l'archive. Utilisez stat (2) sous Linux ou Mac et GetFileTime sous Windows. Ou ajoutez un indicateur de ligne de commande qui permet Ă  l'utilisateur de dĂ©finir une heure spĂ©cifique pour les modifications de fichier.
  • gzip-. — , Deflate ( ). RFC 1952.
  • Zip- . HWZip , read_file mmap(2) Linux Mac CreateFileMapping Windows.
  • HWZip , Zip64. appnote.txt.



All Articles