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 memcpy
ou memmove
.
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:
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 dst
suffisamment 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 head
contient la valeur de hachage du préfixe à trois caractères pour la position dans l'entrée, et in prev
contient 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 x
la liste.#define LZ_WND_SIZE 32768
#define LZ_MAX_LEN 258
#define HASH_SIZE 15
#define NO_POS SIZE_MAX
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;
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;
hash ^= c;
hash &= (1U << HASH_SIZE) - 1;
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.
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) {
prev_match_len = 2;
}
if (prev_match_len >= max_match_len) {
return 0;
}
if (prev_match_len >= 32) {
max_match_steps /= 4;
}
found = false;
i = head[hash];
while (max_match_steps != 0) {
if (i == NO_POS) {
break;
}
assert(i < pos && "Matches should precede pos.");
if (pos - i > LZ_WND_SIZE) {
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) {
return l;
}
}
i = prev[i % LZ_WND_SIZE];
max_match_steps--;
}
if (!found) {
return 0;
}
return prev_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);
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);
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_compress
ce code pour rechercher les correspondances précédentes:
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]);
match_len = find_match(src, i, h, prev_match_len,
min(LZ_MAX_LEN, len - i), head, prev,
&match_pos);
insert_hash(h, i, head, prev);
if (prev_match_len != 0 && prev_match_len >= match_len) {
dist = (i - 1) - prev_match_pos;
if (!backref_callback(dist, prev_match_len, aux)) {
return false;
}
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 (match_len == 0) {
assert(prev_match_len == 0);
if (!lit_callback(src[i], aux)) {
return false;
}
continue;
}
if (prev_match_len != 0) {
if (!lit_callback(src[i - 1], aux)) {
return false;
}
}
prev_match_len = match_len;
prev_match_pos = match_pos;
}
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;
}
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: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: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: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: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.
static void swap32(uint32_t *a, uint32_t *b)
{
uint32_t tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
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 (i * 2 <= n) {
left = i * 2;
right = i * 2 + 1;
min = left;
if (right <= n && heap[right] < heap[left]) {
min = right;
}
if (heap[min] < heap[i]) {
swap32(&heap[min], &heap[i]);
i = min;
} else {
break;
}
}
}
static void minheap_heapify(uint32_t *heap, size_t n)
{
size_t i;
for (i = n / 2; i >= 1; i--) {
minheap_down(heap, n, i);
}
}
Pour suivre la fréquence des n
caractè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 n
tas 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
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:
h = 0;
for (i = 0; i < n; i++) {
freq = freqs[i];
if (freq == 0) {
continue;
}
if (freq > freq_cap) {
freq = freq_cap;
}
h++;
nodes[h] = ((uint32_t)freq << 16) | (uint32_t)(n + h);
}
minheap_heapify(nodes, h);
if (h < 2) {
for (i = 0; i < n; i++) {
lengths[i] = (freqs[i] == 0) ? 0 : 1;
}
return;
}
while (h > 1) {
p = nodes[1];
nodes[1] = nodes[h--];
minheap_down(nodes, h, 1);
q = nodes[1];
nodes[1] = ((p & 0xffff0000) + (q & 0xffff0000))
| (uint32_t)(h + 1);
nodes[p & 0xffff] = nodes[q & 0xffff] = (uint32_t)(h + 1);
minheap_down(nodes, h, 1);
}
h = 0;
for (i = 0; i < n; i++) {
if (freqs[i] == 0) {
lengths[i] = 0;
continue;
}
h++;
p = nodes[n + h];
l = 1;
while (p != 2) {
l++;
p = nodes[p];
}
if (l > max_len) {
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: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: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: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.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
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;
for (i = 0; i < n; i++) {
count[lengths[i]]++;
}
count[0] = 0;
code[0] = 0;
for (l = 1; l <= MAX_HUFFMAN_BITS; l++) {
code[l] = (uint16_t)((code[l - 1] + count[l - 1]) << 1);
}
for (i = 0; i < n; i++) {
l = lengths[i];
if (l == 0) {
continue;
}
codewords[i] = reverse16(code[l]++, l);
}
}
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];
uint8_t lengths[MAX_HUFFMAN_SYMBOLS];
};
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:
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: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».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: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;
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
typedef struct huffman_decoder_t huffman_decoder_t;
struct huffman_decoder_t {
struct {
uint16_t sym : 9;
uint16_t len : 7;
} table[1U << HUFFMAN_LOOKUP_TABLE_BITS];
uint16_t sentinel_bits[MAX_HUFFMAN_BITS + 1];
uint16_t offset_first_sym_idx[MAX_HUFFMAN_BITS + 1];
uint16_t syms[MAX_HUFFMAN_SYMBOLS];
#ifndef NDEBUG
size_t num_syms;
#endif
};
static inline uint64_t lsb(uint64_t x, int n)
{
assert(n >= 0 && n <= 63);
return x & (((uint64_t)1 << n) - 1);
}
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;
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;
}
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:
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
for (i = 0; i < sizeof(d->table) / sizeof(d->table[0]); i++) {
d->table[i].len = 0;
}
for (i = 0; i < n; i++) {
assert(lengths[i] <= MAX_HUFFMAN_BITS);
count[lengths[i]]++;
}
count[0] = 0;
code[0] = 0;
sym_idx[0] = 0;
for (l = 1; l <= MAX_HUFFMAN_BITS; l++) {
code[l] = (uint16_t)((code[l - 1] + count[l - 1]) << 1);
if (count[l] != 0 && code[l] + count[l] - 1 > (1U << l) - 1) {
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];
}
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);
pad_len = HUFFMAN_LOOKUP_TABLE_BITS - len;
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 :
typedef struct istream_t istream_t;
struct istream_t {
const uint8_t *src;
const uint8_t *end;
size_t bitpos;
size_t bitpos_end;
};
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)
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) {
bits = read64le(next);
} else {
bits = 0;
for (i = 0; i < is->end - next; i++) {
bits |= (uint64_t)next[i] << (i * 8);
}
}
return bits >> (is->bitpos % 8);
}
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_bits
vous 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_t
sont 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):
static inline uint64_t read64le(const uint8_t *p)
{
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:
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);
}
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.
typedef struct ostream_t ostream_t;
struct ostream_t {
uint8_t *dst;
uint8_t *end;
size_t bitpos;
size_t bitpos_end;
};
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;
}
static inline size_t ostream_bit_pos(const ostream_t *os)
{
return os->bitpos;
}
static inline size_t ostream_bytes_written(ostream_t *os)
{
return round_up(os->bitpos, 8) / 8;
}
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) {
return false;
}
p = &os->dst[os->bitpos / 8];
shift = os->bitpos % 8;
if (os->end - p >= 8) {
x = read64le(p);
x = lsb(x, shift);
x |= bits << shift;
write64le(p, x);
} else {
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;
}
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
:
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,
HWINF_FULL,
HWINF_ERR
} inf_stat_t;
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 {
bits = istream_bits(&is);
if (!istream_advance(&is, 3)) {
return HWINF_ERR;
}
bfinal = bits & 1;
bits >>= 1;
switch (lsb(bits, 2)) {
case 0:
s = inf_noncomp_block(&is, dst, dst_cap, &dst_pos);
break;
case 1:
s = inf_fixed_block(&is, dst, dst_cap, &dst_pos);
break;
case 2:
s = inf_dyn_block(&is, dst, dst_cap, &dst_pos);
break;
default:
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);
if (!istream_advance(is, 32)) {
return HWINF_ERR;
}
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;
}
if (dst_cap - *dst_pos < len) {
return HWINF_FULL;
}
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.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):
struct litlen_tbl_t {
uint16_t base_len : 9;
uint16_t ebits : 7;
};
const struct litlen_tbl_t litlen_tbl[29] = {
{ 3, 0 },
{ 4, 0 },
...
{ 227, 5 },
{ 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):Le décompresseur stocke ces longueurs dans une table pratique pour la transmission à huffman_decoder_init
:const uint8_t fixed_litlen_lengths[288] = {
8,
8,
...
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: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] = {
5,
5,
...
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) {
bits = istream_bits(is);
litlen = huffman_decode(litlen_dec, (uint16_t)bits, &used);
bits >>= used;
used_tot = used;
if (litlen < 0 || litlen > LITLEN_MAX) {
return HWINF_ERR;
} else if (litlen <= UINT8_MAX) {
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) {
if (!istream_advance(is, used_tot)) {
return HWINF_ERR;
}
return HWINF_OK;
}
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);
distsym = huffman_decode(dist_dec, (uint16_t)bits, &used);
bits >>= used;
used_tot += used;
if (distsym < 0 || distsym > DISTSYM_MAX) {
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;
}
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.
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) {
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
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);
num_litlen_lens = lsb(bits, 5) + MIN_LITLEN_LENS;
bits >>= 5;
assert(num_litlen_lens <= MAX_LITLEN_LENS);
num_dist_lens = lsb(bits, 5) + MIN_DIST_LENS;
bits >>= 5;
assert(num_dist_lens <= MAX_DIST_LENS);
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.
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.
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) {
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) {
if (i < 1) {
return HWINF_ERR;
}
n = lsb(bits, 2) + CODELEN_COPY_MIN;
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) {
n = lsb(bits, 3) + CODELEN_ZEROS_MIN;
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) {
n = lsb(bits, 7) + CODELEN_ZEROS2_MIN;
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 {
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:
#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;
size_t block_len;
size_t block_len_bytes;
uint16_t litlen_freqs[LITLEN_MAX + 1];
uint16_t dist_freqs[DISTSYM_MAX + 1];
struct {
uint16_t distance;
union {
uint16_t lit;
uint16_t len;
} 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_compress
nous 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];
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;
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) {
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;
}
len = s->block[i].u.len;
litlen = len2litlen[len];
bits = litlen_enc->codewords[litlen];
nbits = litlen_enc->lengths[litlen];
ebits = len - litlen_tbl[litlen - LITLEN_TBL_OFFSET].base_len;
bits |= ebits << nbits;
nbits += litlen_tbl[litlen - LITLEN_TBL_OFFSET].ebits;
distance = s->block[i].distance;
dist = distance2dist[distance];
bits |= (uint64_t)dist_enc->codewords[dist] << nbits;
nbits += dist_enc->lengths[dist];
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;
};
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.
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;
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);
while (dist_lens[*num_dist_lens - 1] == 0 && *num_dist_lens > 1) {
(*num_dist_lens)--;
}
assert(*num_dist_lens >= MIN_DIST_LENS);
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.
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) {
for (j = i; j < min(n, i + CODELEN_ZEROS2_MAX) &&
lens[j] == 0; j++);
count = (uint8_t)(j - i);
if (count < CODELEN_ZEROS_MIN) {
encoded[num_encoded++].sym = 0;
i++;
continue;
}
if (count <= CODELEN_ZEROS_MAX) {
assert(count >= CODELEN_ZEROS_MIN &&
count <= CODELEN_ZEROS_MAX);
encoded[num_encoded].sym = CODELEN_ZEROS;
encoded[num_encoded++].count = count;
} else {
assert(count >= CODELEN_ZEROS2_MIN &&
count <= CODELEN_ZEROS2_MAX);
encoded[num_encoded].sym = CODELEN_ZEROS2;
encoded[num_encoded++].count = count;
}
i = j;
continue;
}
encoded[num_encoded++].sym = lens[i++];
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) {
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 };
size_t count_codelen_lens(const uint8_t *codelen_lens)
{
size_t n = MAX_CODELEN_LENS;
while (codelen_lens[codelen_lengths_order[n - 1]] == 0) {
n--;
}
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;
bits = (0x2 << 1) | final;
nbits = 3;
hlit = num_litlen_lens - MIN_LITLEN_LENS;
bits |= hlit << nbits;
nbits += 5;
hdist = num_dist_lens - MIN_DIST_LENS;
bits |= hdist << nbits;
nbits += 5;
hclen = num_codelen_lens - MIN_CODELEN_LENS;
bits |= hclen << nbits;
nbits += 4;
if (!ostream_write(&s->os, bits, nbits)) {
return false;
}
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;
}
}
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) {
bits |= (count - CODELEN_COPY_MIN) << nbits;
nbits += 2;
} else if (sym == CODELEN_ZEROS) {
bits |= (count - CODELEN_ZEROS_MIN) << nbits;
nbits += 3;
} else if (sym == CODELEN_ZEROS2) {
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:
static size_t uncomp_block_len(const deflate_state_t *s)
{
size_t bit_pos, padding;
bit_pos = ostream_bit_pos(&s->os) + 3;
padding = round_up(bit_pos, 8) - bit_pos;
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:
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:
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;
len = 3;
len += 5 + 5 + 4;
len += 3 * num_codelen_lens;
for (i = 0; i < MAX_CODELEN_LENS; i++) {
freq = codelen_freqs[i];
len += codelen_enc->lengths[i] * freq;
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:
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);
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);
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);
num_encoded_lens = encode_dist_litlen_lens(dyn_litlen_enc.lengths,
dyn_dist_enc.lengths,
encoded_lens,
&num_litlen_lens,
&num_dist_lens);
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:
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;
}
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 :- 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.
- . , , , Zip-.
- , . , . 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:
struct eocdr {
uint16_t disk_nbr;
uint16_t cd_start_disk;
uint16_t disk_cd_entries;
uint16_t cd_entries;
uint32_t cd_size;
uint32_t cd_offset;
uint16_t comment_len;
const uint8_t *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:
#define READ16(p) ((p) += 2, read16le((p) - 2))
#define READ32(p) ((p) += 4, read32le((p) - 4))
#define EOCDR_BASE_SZ 22
#define EOCDR_SIGNATURE 0x06054b50
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:
#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)
struct cfh {
uint16_t made_by_ver;
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;
uint16_t comment_len;
uint16_t disk_nbr_start;
uint16_t int_attrs;
uint32_t ext_attrs;
uint32_t lfh_offset;
const uint8_t *name;
const uint8_t *extra;
const uint8_t *comment;
};
made_by_ver
et extract_ver
encoder 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_flag
contient 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).
method
code 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_time
et mod_date
contiennent 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_t
vers et depuis le format MS-DOS:
static time_t dos2ctime(uint16_t dos_date, uint16_t dos_time)
{
struct tm tm = {0};
tm.tm_sec = (dos_time & 0x1f) * 2;
tm.tm_min = (dos_time >> 5) & 0x3f;
tm.tm_hour = (dos_time >> 11);
tm.tm_mday = (dos_date & 0x1f);
tm.tm_mon = ((dos_date >> 5) & 0xf) - 1;
tm.tm_year = (dos_date >> 9) + 80;
tm.tm_isdst = -1;
return mktime(&tm);
}
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;
*dos_time |= tm->tm_min << 5;
*dos_time |= tm->tm_hour << 11;
*dos_date = 0;
*dos_date |= tm->tm_mday;
*dos_date |= (tm->tm_mon + 1) << 5;
*dos_date |= (tm->tm_year - 80) << 9;
}
Le champ crc32
contient 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_size
et uncomp_size
contiennent 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_start
Conçu pour les archives utilisant plusieurs disquettes.int_attrs
et ext_attrs
dé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_offset
nous 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). extra
peut 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:
#define CFH_BASE_SZ 46
#define CFH_SIGNATURE 0x02014b50
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:
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:
#define LFH_BASE_SZ 30
#define LFH_SIGNATURE 0x04034b50
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;
typedef struct zip_t zip_t;
struct zip_t {
uint16_t num_members;
const uint8_t *comment;
uint16_t comment_len;
zipiter_t members_begin;
zipiter_t members_end;
const uint8_t *src;
size_t src_len;
};
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;
}
zip->num_members = eocdr.cd_entries;
zip->comment = eocdr.comment;
zip->comment_len = eocdr.comment_len;
offset = eocdr.cd_offset;
zip->members_begin = offset;
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;
}
if (cfh.method != ZIP_STORED && cfh.method != ZIP_DEFLATED) {
return false;
}
if (cfh.method == ZIP_STORED &&
cfh.uncomp_size != cfh.comp_size) {
return false;
}
if (cfh.disk_nbr_start != 0) {
return false;
}
if (memchr(cfh.name, '\0', cfh.name_len) != NULL) {
return false;
}
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;
}
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;
uint16_t name_len;
time_t mtime;
uint32_t comp_size;
const uint8_t *comp_data;
method_t method;
uint32_t uncomp_size;
uint32_t crc32;
const uint8_t *comment;
uint16_t comment_len;
bool is_dir;
zipiter_t next;
};
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:
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;
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;
total += LFH_BASE_SZ + name_len;
total += file_sizes[i];
}
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:
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;
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;
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);
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);
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 main
vé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.