Archivos Zip: historial, explicación e implementación



Hace tiempo que me pregunto cómo se comprimen los datos, incluso en los archivos Zip. Una vez decidí satisfacer mi curiosidad: aprender cómo funciona la compresión y escribir mi propio programa Zip. La implementación se ha convertido en un ejercicio emocionante en la programación. Obtiene un gran placer al crear una máquina depurada que toma datos, transfiere sus bits a una representación más eficiente y luego los recupera. Espero que también te interese leer sobre esto.

El artículo explica con gran detalle cómo funcionan los archivos Zip y el esquema de compresión: compresión LZ77, algoritmo Huffman, algoritmo Deflate y más. Aprenderá la historia del desarrollo de la tecnología y verá ejemplos de implementación bastante efectivos escritos desde cero en C. El código fuente está aquí: hwzip-1.0.zip .

Estoy muy agradecido con Ange Albertini , Gynvael Coldwind , Fabian Giesen , Jonas Skeppstedt ( web ), Primiano Tucci y Nico Weber , quienes brindaron valiosos comentarios sobre los borradores de este artículo.

Contenido



Historia


PKZip


En los años ochenta y principios de los noventa, antes de que Internet se generalizara, los entusiastas de las computadoras usaban módems de acceso telefónico para conectarse a través de la red telefónica a la red de Sistemas de Tablero de Boletines (BBS). BBS era un sistema informático interactivo que permitía a los usuarios enviar mensajes, jugar juegos y compartir archivos. Para conectarse, fue suficiente tener una computadora, un módem y un buen número de teléfono de BBS. Los números se publicaron en revistas de informática y en otros BBS.

Una herramienta importante para facilitar la distribución de archivos fue el archivador . Le permite guardar uno o más archivos en un único archivopara almacenar o transmitir información de manera más conveniente. E idealmente, el archivo también comprimió archivos para ahorrar espacio y tiempo para la transmisión a través de la red. En los días de BBS, el archivador Arc era popular, escrito por Tom Henderson de System Enhancement Associates (SEA), una pequeña empresa que fundó con su cuñado.

A fines de la década de 1980, el programador Phil Katz lanzó su propia versión de Arc, PKArc. Era compatible con SEA Arc, pero funcionaba más rápido gracias a las subrutinas escritas en lenguaje ensamblador y utilizaba un nuevo método de compresión. El programa se hizo popular, Katz renunció a su trabajo y creó PKWare para enfocarse en un mayor desarrollo. Según la leyenda, la mayor parte del trabajo tuvo lugar en la cocina de su madre en Glendale, Wisconsin.


Foto de Phil Katz de un artículo en el Milwaukee Sentinel , 19 de septiembre de 1994.

Sin embargo, la SEA no estaba contenta con la iniciativa de Katz. La compañía lo acusó de infracción de marca registrada y derechos de autor. El litigio y la controversia en la red BBS y el mundo de las PC se ha conocido como Arc Wars . Al final, la disputa se resolvió a favor de la SEA.

Abandonando Arc, Katz creó un nuevo formato de archivo en 1989, que llamó Zip y puso a disposición del público :

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

El programa de Katz para crear dichos archivos se llamaba PKZip y pronto se extendió al mundo de BBS y PC.

Uno de los aspectos que probablemente contribuyó al éxito del formato Zip es que la documentación vino con PKZip, Application Note , que explica en detalle cómo funciona el formato. Esto permitió a otros aprender el formato y crear programas que generan, extraen o interactúan con archivos Zip.

Zip: un formato de compresión sin pérdida : después de desempacar los datos serán los mismos que antes de la compresión. El algoritmo busca redundancia en los datos de origen y presenta información de manera más eficiente. Este enfoque es diferente de la compresión con pérdida., que se usa en formatos como JPEG y MP3: cuando se comprime, se desecha parte de la información que es menos notable para el ojo humano o el oído.

PKZip se distribuyó como Shareware: se podía usar y copiar libremente, pero el autor sugirió a los usuarios que "registraran" el programa. Por $ 47, puede obtener instrucciones impresas, soporte premium y una versión mejorada de la aplicación.


Una de las versiones clave de PKZip fue 2.04c, lanzada el 28 de diciembre de 1992 (la versión 2.04g fue lanzada poco después ). Usó el algoritmo de compresión Deflate predeterminado. La versión determinó el desarrollo posterior de la compresión en archivos Zip ( artículo dedicado al lanzamiento ).


Desde entonces, el formato zip se ha utilizado en muchos otros formatos de archivo. Por ejemplo, los archivos Java (.jar), los paquetes de aplicaciones de Android (.apk) y los archivos .docx de Microsoft Office usan el formato Zip. Muchos formatos y protocolos usan el mismo algoritmo de compresión, Deflate. Digamos que las páginas web probablemente se transfieren a su navegador como un archivo gzip, cuyo formato utiliza la compresión Deflate.

Phil Katz murió en 2000. PKWare todavía existe y es compatible con el formato Zip, aunque la compañía se centra principalmente en el software de protección de datos.

Info-zip y zlib


Poco después del lanzamiento de PKZip en 1989, comenzaron a aparecer otros programas para descomprimir archivos Zip. Por ejemplo, un programa de descompresión que podría descomprimir en sistemas Unix. En marzo de 1990, se creó una lista de correo llamada Info-ZIP.

El grupo Info-ZIP ha lanzado programas gratuitos de código abierto para descomprimir y comprimir , que se utilizaron para descomprimir y crear archivos zip. El código ha sido portado a muchos sistemas, y sigue siendo el estándar para los programas Zip para sistemas Unix. Esto más tarde ayudó a aumentar la popularidad de los archivos Zip.

Una vez que el código ZIP de información que desinfló la compresión y descompresión se movió a una biblioteca zlib separada que escribieronJean-loup Gailly (compresión) y Mark Adler (desembalaje).


Jean-loup Gailly (izquierda) y Mark Adler (derecha) en el Premio USENIX STUG 2009 .

Una de las razones para crear la biblioteca fue que proporcionaba la conveniencia de usar la compresión Deflate en otras aplicaciones y formatos, por ejemplo, en los nuevos gzip y PNG . Estos nuevos formatos estaban destinados a reemplazar a Compress y GIF , que utilizaban el algoritmo LZW protegido por patente.

Como parte de la creación de estos formatos, Peter Deutsch escribió la especificación Deflate y la publicó bajo el nombre de Internet RFC 1951 en mayo de 1996. Resultó ser una descripción más accesible en comparación con la Nota de aplicación PKZip original.

Hoy zlib se usa en todas partes. Quizás ahora sea responsable de comprimir esta página en un servidor web y desempacarla en su navegador. Hoy, la mayoría de los archivos zip se comprimen y descomprimen usando zlib.

Winzip


Muchos de los que no encontraron PKZip usaron WinZip. Los usuarios de PC cambiaron de DOS a Windows, y de PKZip a WinZip.

Todo comenzó con un proyecto del programador Nico Mac, que creó software para OS / 2 en Mansfield Software Group en Storrs-Mansfield, Connecticut. Nico usó Presentation Manager, esta es una interfaz gráfica de usuario en OS / 2, y estaba molesto porque tenía que cambiar de un administrador de archivos a comandos de DOS cada vez que quería crear archivos Zip.

Mac escribió un programa GUI simple que funcionaba con archivos Zip directamente en Presentation Manager, lo llamó PMZip y lo lanzó como shareware en la década de 1990.

OS / 2 no tuvo éxito, y el mundo de las PC se hizo cargo de Microsoft Windows. En 1991, Mac decidió aprender a escribir programas de Windows, y su primer proyecto fue portar su aplicación Zip a un nuevo sistema operativo. En abril de 1991, se lanzó WinZip 1.00 . Se distribuyó como shareware con un período de prueba de 21 días y una tarifa de registro de $ 29. Ella se veía así:


En las primeras versiones de WinZip, PKZip se utilizó debajo del capó. Pero a partir de la versión 5.0 en 1993, el código de Info-ZIP comenzó a usarse para el procesamiento directo de archivos Zip. La interfaz de usuario también ha evolucionado gradualmente.


WinZip 6.3 en Windows 3.11 para grupos de trabajo.

WinZip fue uno de los programas shareware más populares en la década de 1990. Pero al final, perdió relevancia debido a la incorporación de soporte para archivos Zip en los sistemas operativos. Windows ha estado trabajando con ellos como "carpetas comprimidas" desde 2001 (Windows XP), la biblioteca DynaZip se utiliza para esto.

Mac originalmente se llamaba Nico Mak Computing. En 2000, pasó a llamarse WinZip Computing, y por esos años Mack lo dejó. En 2005, Vector Capital vendió la compañíay, al final, se convirtió en propiedad de Corel , que aún lanza WinZip como producto.

Compresión Lempel-Ziv (LZ77)


La compresión Zip consta de dos ingredientes principales: compresión Lempel-Ziv y código Huffman.

Una forma de comprimir el texto es crear una lista de palabras o frases comunes con el reemplazo de variedades de estas palabras dentro del texto con enlaces al diccionario. Por ejemplo, la palabra larga "compresión" en el texto fuente se puede representar como # 1234, donde 1234 se refiere a la posición de la palabra en la lista. Esto se llama compresión de diccionario .

Pero desde el punto de vista de la compresión universal, este método tiene varios inconvenientes. Primero, ¿qué debería entrar exactamente en el diccionario? Los datos de origen pueden estar en diferentes idiomas, incluso puede no ser texto legible por humanos. Y si el diccionario no se acuerda de antemano entre la compresión y la descompresión, entonces deberá almacenarse y transferirse junto con los datos comprimidos, lo que reduce los beneficios de la compresión.

Una solución elegante a este problema es utilizar los datos de origen como un diccionario. En 1977, un algoritmo universal para la compresión secuencial de datos , Jacob Ziv y Abraham Lempel (que trabajaban en Technion), propusieron un esquema de compresión en el que los datos de origen se presentan como una secuencia de tripletes:

(, , )

donde se forman un vínculo de retroceso a la secuencia de caracteres que desea copiar desde la posición anterior en el texto original, y este es el siguiente carácter de los datos generados.


Abraham Lempel y Jacob Ziv.

Considere las siguientes líneas:

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

En la segunda línea, la secuencia "t was the w" se puede representar como (26, 10, w), ya que se recrea copiando 10 caracteres de la posición de 26 caracteres a la letra "w". Para los caracteres que aún no han aparecido, se utilizan vínculos de retroceso de longitud cero. Por ejemplo, la "I" inicial se puede representar como (0, 0, I).

Este esquema se llama compresión Lempel-Ziv o compresión LZ77. Sin embargo, en implementaciones prácticas del algoritmo, parte del triplete generalmente no se usa . En cambio, los caracteres se generan individualmente y se usan pares ( , ) para los vínculos de retroceso (esta opción se denomina compresión LZSS ). La forma en que se codifican los literales y los vínculos de retroceso es un tema separado, lo consideraremos a continuación cuando analicemos el algoritmoEl desinflar .

Este texto:

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

Puedes comprimir esto:

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

Una de las propiedades importantes de los vínculos de retroceso es que pueden superponerse. Esto sucede cuando la longitud es mayor que la distancia. Por ejemplo:

Fa-la-la-la-la

Puedes comprimir a:

Fa-la(3,9)

Puede parecerle extraño, pero el método funciona: después de copiar los bytes de los primeros tres "-la", la copia continúa utilizando los bytes recién generados.

De hecho, este es un tipo de codificación de longitudes de series , en el que parte de los datos se copian repetidamente para obtener la longitud deseada.

En un artículo de Colin Morris, se muestra un ejemplo interactivo del uso de la compresión Lempel-Ziv para las letras de canciones . .

El siguiente es un ejemplo de copia de vínculos de retroceso en C. Tenga en cuenta que debido a una posible superposición, no podemos usar memcpyo memmove.

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

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

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

Es fácil generar literales, pero para completar, utilizaremos una función auxiliar:

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

Tenga en cuenta que la persona que llama de esta función debe asegurarse de que haya dstsuficiente espacio para los datos generados y que el vínculo de retroceso no acceda a la posición antes del inicio del búfer.

Es difícil no generar datos utilizando vínculos de retroceso durante el desempaquetado, sino crearlos primero al comprimir los datos de origen. Esto se puede hacer de diferentes maneras, pero utilizaremos el método basado en tablas hash de zlib, que se propone en RFC 1951.

Usaremos una tabla hash con las posiciones de los prefijos de tres caracteres que se encontraron previamente en la línea (los vínculos de retroceso más cortos no aportan ningún beneficio). Deflate permite vínculos de retroceso dentro de los 32.768 caracteres anteriores; esto se denomina ventana. Esto proporciona compresión de transmisión: los datos de entrada se procesan poco a poco, siempre que la ventana con los últimos bytes se almacene en la memoria. Sin embargo, nuestra implementación asume que todos los datos de entrada están disponibles para nosotros y que podemos procesarlos completos a la vez. Esto le permite centrarse en la compresión en lugar de la contabilidad, lo cual es necesario para el procesamiento de flujo.

Usaremos dos matrices: in headcontiene el valor hash del prefijo de tres caracteres para la posición en la entrada, y in prevcontiene la posición de la posición anterior con este valor hash. De hecho, head[h]este es el encabezado de una lista vinculada de posiciones de prefijo con un hash h, y prev[x]recibe el elemento que precede a xla lista.

#define LZ_WND_SIZE 32768
#define LZ_MAX_LEN  258

#define HASH_SIZE 15
#define NO_POS    SIZE_MAX

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

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

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

Para insertar una nueva posición de cadena en la tabla hash prev, se actualiza para indicar la anterior head, y luego se actualiza a sí misma head:

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

Preste atención a la operación de módulo al indexar prev: solo nos interesan las posiciones que se encuentran en la ventana actual.

En lugar de calcular el valor hash para cada prefijo de tres caracteres desde cero, usaremos un hash de anillo y lo actualizaremos constantemente para que solo los últimos tres caracteres se reflejen en su valor:

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

        return hash;
}

El mapa hash se puede usar para buscar eficientemente coincidencias anteriores con una secuencia, como se muestra a continuación. La búsqueda de coincidencias es la operación de compresión que requiere más recursos, por lo que limitaremos la profundidad de la búsqueda en la lista.

Cambiar varios parámetros, como la profundidad de búsqueda en la lista de prefijos y realizar comparaciones perezosas, como se describe a continuación, es una forma de aumentar la velocidad al reducir la relación de compresión. La configuración de nuestro código se selecciona para que coincida con el nivel de compresión máximo en zlib.

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

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

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

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

        found = false;
        i = head[hash];

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

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

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

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

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

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

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

        if (!found) {
                return 0;
        }

        return prev_match_len;
}

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

        assert(prev_match_len < max_match_len);

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

        assert(l == prev_match_len + 1);

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

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

        return l;
}

Puede terminar la función con lz77_compresseste código para buscar coincidencias anteriores:

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

        prev_match_len = 0;
        prev_match_pos = 0;

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

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

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

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

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

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

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

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

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

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

        return true;
}

Este código busca el vínculo de retroceso más largo que se puede generar en la posición actual. Pero antes de emitirlo, el programa decide si es posible encontrar una coincidencia aún más larga en la siguiente posición. En zlib, esto se llama evaluación de comparación perezosa . Esto sigue siendo un algoritmo codicioso : selecciona la coincidencia más larga, incluso si la actual más corta le permite obtener una coincidencia aún más larga y lograr una compresión más fuerte.

La compresión Lempel-Ziv puede funcionar tanto rápido como lento. ZopfliPasé mucho tiempo buscando vínculos de retroceso óptimos para exprimir porcentajes de compresión adicionales. Esto es útil para datos que se comprimen una vez y luego se reutilizan, por ejemplo, para información estática en un servidor web. En el otro lado de la escala hay compresores como Snappy y LZ4 , que se comparan solo con el último prefijo de 4 bytes y son muy rápidos. Este tipo de compresión es útil en bases de datos y sistemas RPC en los que el tiempo dedicado a la compresión vale la pena al ahorrar tiempo al enviar datos a través de una red o al disco.

La idea de utilizar datos de origen como diccionario es muy elegante, pero también puede beneficiarse de un diccionario estático. Brotli es un algoritmo basado en LZ77, pero también utiliza un grandiccionario estático de cadena, que a menudo se encuentra en la red.

LZ77 código se puede ver en lz77.h y lz77.c .

Código Huffman


El segundo algoritmo de compresión Zip es el código Huffman.

El término código en este contexto es una referencia a un sistema para presentar datos en alguna otra forma. En este caso, estamos interesados ​​en el código que se puede utilizar para representar de manera eficiente los literales y los vínculos de retroceso generados por el algoritmo Lempel-Ziv.

Tradicionalmente, el texto en inglés se presenta utilizando el Código Estándar Americano para el Intercambio de Información (ASCII) . Este sistema asigna a cada carácter un número, que generalmente se almacena en una representación de 8 bits. Aquí están los códigos ASCII para las letras mayúsculas del alfabeto inglés:

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

Un byte por carácter es una forma conveniente de almacenar texto. Le permite acceder o modificar fácilmente partes del texto, y siempre está claro cuántos bytes se requieren para almacenar N caracteres, o cuántos caracteres se almacenan en N bytes. Sin embargo, esta no es la forma más efectiva en términos de espacio ocupado. Por ejemplo, en inglés, la letra E se usa con mayor frecuencia y Z es la menos utilizada. Por lo tanto, en términos de volumen, es más eficiente usar una representación de bits más corta para E y una más larga para Z, en lugar de asignar el mismo número de bits a cada carácter.

Un código que especifica codificaciones de diferentes longitudes a diferentes caracteres de origen se denomina código de longitud variable . El ejemplo más famoso es el código Morse., en el que cada carácter está codificado con puntos y guiones, originalmente transmitidos por telégrafo con pulsos cortos y largos:

UNA• -norte- •
si- • • •O- - -
C- • - •PAGS• - - •
re- • •Q- - • -
miR• - •
F• • - •S• • •
sol- - •T-
H• • • •U• • -
yo• •V• • • -
J• - - -W• - -
K- • -X- • • -
L• - • •Y- • - -
METRO- -Z- - • •

Uno de los inconvenientes del código Morse es que una palabra de código puede ser un prefijo de otra. Por ejemplo, • • - • no tiene una decodificación única: puede ser F o ER. Esto se resuelve mediante pausas (tres puntos de longitud) entre las letras durante la transmisión. Sin embargo, sería mejor si las palabras de código no pudieran ser prefijos de otras palabras. Este código se llama sin corregir . El código ASCII de una longitud fija no se fija porque las palabras de código siempre tienen la misma longitud. Pero los códigos de longitud variable también pueden no repararse. Los números de teléfono a menudo no se corrigen. Antes de que se introdujera el número telefónico de emergencia 112 en Suecia, todos los números que comenzaban con el 112 tuvieron que cambiarse, y en los Estados Unidos no hay un solo número telefónico que comience con el 911.

Para minimizar el tamaño del mensaje codificado, es mejor usar un código no fijado en el que los caracteres frecuentes tienen palabras de código más cortas. El código óptimo será el que genere el resultado más corto posible: la suma de las longitudes de las palabras de código, multiplicadas por su frecuencia de aparición, será la mínima posible. Esto se llama un código sin prefijo con redundancia mínima , o un código Huffman , en honor del inventor de un algoritmo eficiente para generar dichos códigos.

Algoritmo Huffman


Mientras estudiaba materiales para escribir su tesis doctoral sobre ingeniería electrónica en el MIT, David Huffman asistió a un curso sobre teoría de la información impartido por Robert Fano. Según la leyenda , Fano permitió a sus alumnos elegir: escribir el examen o curso final. Huffman eligió este último, y se le dio el tema de buscar códigos sin prefijo con una redundancia mínima. Se supone que no sabía que el propio Fano estaba trabajando en esta tarea en ese momento (el algoritmo de Shannon-Fano fue el método más famoso en esos años ). El trabajo de Huffman se publicó en 1952 bajo el título Un método para la construcción de códigos de redundancia mínima en 1952. Y desde entonces su algoritmo ha sido ampliamente utilizado.


Comunicado de prensa de David Huffman UC Santa Cruz.

El algoritmo de Huffman crea código no fijado con redundancia mínima para el conjunto de caracteres y su frecuencia de uso. El algoritmo selecciona repetidamente dos caracteres que es menos probable que se encuentren en los datos de origen, por ejemplo, X e Y, y los reemplaza con un carácter compuesto que significa "X o Y". La frecuencia de aparición de un símbolo compuesto es la suma de las frecuencias de dos símbolos fuente. Las palabras de código para X e Y pueden ser cualquier palabra de código asignada al carácter compuesto "X o Y" seguido de 0 o 1 para distinguir los caracteres originales. Cuando los datos de entrada se reducen a un carácter, el algoritmo deja de funcionar ( explicación en video ).

Aquí hay un ejemplo del algoritmo que funciona en un pequeño conjunto de caracteres:

SímboloFrecuencia
UNA6 6
si4 4
C2
re3

Primera iteración de procesamiento:


Los dos símbolos más raros, C y D, se eliminan del conjunto y se reemplazan por un símbolo compuesto cuya frecuencia es la suma de las frecuencias C y D:


Ahora los símbolos más raros son B y un símbolo compuesto con una frecuencia de 5. Se eliminan del conjunto y se reemplazan con un símbolo compuesto con una frecuencia de 9:


Finalmente, A y un símbolo compuesto con una frecuencia de 9 se combinan en un nuevo símbolo con una frecuencia de 15:


Todo el conjunto se redujo a un personaje, el procesamiento se ha completado.

El algoritmo creó una estructura llamada el árbol Huffman . Los caracteres de entrada son hojas, y cuanto mayor es la frecuencia de un carácter, mayor es su ubicación. A partir de la raíz del árbol, puede generar palabras de código para los caracteres agregando 0 o 1 al moverse hacia la izquierda o hacia la derecha, respectivamente. Resulta así:

SímboloLa palabra clave
UNA0 0
si10
C110
re111

Ninguna palabra de código es un prefijo para ninguna otra. Cuanto más a menudo aparece un símbolo, más corta es su palabra de código.

El árbol también se puede usar para decodificar: comenzamos desde la raíz y vamos hacia la derecha o hacia la izquierda para el valor con 0 o 1 delante del carácter. Por ejemplo, la línea 010100 se decodifica en ABBA.

Tenga en cuenta que la longitud de cada palabra de código es equivalente a la profundidad del nodo del árbol correspondiente. Como veremos en la siguiente parte, no necesitamos un árbol real para asignar palabras de código. Es suficiente saber la longitud de las palabras mismas. Por lo tanto, el resultado de nuestra implementación del algoritmo Huffman será la longitud de las palabras de código.

Para almacenar el conjunto de caracteres y encontrar eficientemente las frecuencias más bajas, utilizaremos la estructura de datos de almacenamiento dinámico binario . En particular, estamos interesados ​​enmin-heap , ya que el valor mínimo debe estar en la parte superior.

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

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

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

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

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

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

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

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

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

Para rastrear la frecuencia de los ncaracteres, utilizaremos un montón de nelementos. Además, cada vez que se crea un símbolo compuesto, queremos "vincular" ambos símbolos de origen. Por lo tanto, cada símbolo tendrá un "elemento de comunicación".

Para almacenar el nmontón de nelementos y los elementos de comunicación, utilizaremos una matriz de n * 2 + 1elementos. Cuando dos caracteres en el montón se reemplazan por uno, usaremos el segundo elemento para guardar el enlace al nuevo personaje. Este enfoque se basa en la implementación de Gestión de Gigabytes de Witten, Moffat y Bell.

En cada nodo del montón, utilizaremos los 16 bits más significativos para almacenar la frecuencia del símbolo, y los 16 bits menos significativos para almacenar el índice del elemento de comunicación del símbolo. Debido al uso de bits altos, la diferencia de frecuencia estará determinada por el resultado de una comparación de 32 bits entre dos elementos del montón.

Debido a esta representación, debemos asegurarnos de que la frecuencia de los caracteres siempre se ajuste a 16 bits. Después de completar el algoritmo, el símbolo compuesto final tendrá la frecuencia de todos los símbolos combinados, es decir, esta suma debe colocarse en 16 bits. Nuestra implementación Deflate verificará esto procesando simultáneamente hasta 64,535 caracteres.

Los símbolos con frecuencia cero recibirán palabras de código de longitud cero y no participarán en la compilación de la codificación.

Si la palabra de código alcanza la profundidad máxima especificada, "suavizaremos" la distribución de frecuencia imponiendo un límite de frecuencia e intentaremos nuevamente (sí, con la ayuda goto). Existen formas más sofisticadas de codificación Huffman con profundidad limitada, pero esta es simple y eficiente.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Una alternativa elegante a la opción de almacenamiento dinámico binario es almacenar caracteres en dos colas. El primero contiene los caracteres de origen, ordenados por frecuencia. Cuando se crea un símbolo compuesto, se agrega de forma secundaria. Por lo tanto, el símbolo con la frecuencia más baja siempre estará en la primera posición de una de las colas. Jan van Leeuwen describe este enfoque en Sobre la construcción de árboles de Huffman (1976).

La codificación de Huffman es óptima para códigos sin prefijo, pero en otros casos existen métodos más eficientes: codificación aritmética y sistemas de números asimétricos .

Códigos canónicos de Huffman


En el ejemplo anterior, construimos un árbol Huffman:


Si vamos desde la raíz y usamos 0 para la rama izquierda y 1 para la derecha, obtenemos los siguientes códigos:

SímboloLa palabra clave
UNA0 0
si10
C110
re111

La decisión de usar 0 para la rama izquierda y 1 para la derecha parece arbitraria. Si hacemos lo contrario, obtenemos:

SímboloLa palabra clave
UNA1
si01
C001
re000

Podemos marcar arbitrariamente dos ramas que se originan en un nodo con cero y uno (lo principal es que las etiquetas son diferentes), y aún así obtener el código equivalente:


SímboloLa palabra clave
UNA0 0
sionce
C100
re101

Aunque el algoritmo Huffman proporciona las longitudes de palabra de código requeridas para código no fijado con redundancia mínima, hay muchas formas de asignar palabras de código individuales.

Dada la longitud de la palabra de código calculada por el algoritmo de Huffman, el código canónico de Huffman asigna palabras de código a los caracteres de una manera específica. Esto es útil porque le permite almacenar y transmitir longitudes de palabras de código con datos comprimidos: el decodificador podrá recuperar palabras de código en función de sus longitudes. Por supuesto, puede almacenar y transmitir frecuencias de símbolos y ejecutar el algoritmo Huffman en el decodificador, pero esto requerirá más trabajo y más almacenamiento desde el decodificador. Otra propiedad muy importante es que la estructura de los códigos canónicos utiliza una decodificación eficiente.

La idea es asignar palabras de código a los caracteres secuencialmente, debajo de uno a la vez. La primera palabra de código es 0. La siguiente será la palabra con la longitud de la palabra anterior + 1. La primera palabra con una longitud de N se compone de la última palabra de longitud N-1, agregando una (para obtener una nueva palabra de código) y un paso a la izquierda (para aumentar la longitud).

En la terminología del árbol de Hoffman, las palabras de código se asignan secuencialmente a las hojas en el orden de izquierda a derecha, un nivel a la vez, desplazándose hacia la izquierda cuando se pasa al siguiente nivel.

En nuestro ejemplo ABCD, el algoritmo Huffman asignó palabras de código con longitudes de 1, 2, 3 y 3. La primera palabra es 0. Esta es también la última palabra de longitud 1. Para la longitud 2, tomamos 0 y agregamos 1 para obtener el siguiente código, que se convertirá en el prefijo de los códigos de dos bits , cambie a la izquierda y obtenga 10. Esta es ahora la última palabra de longitud 2. Para obtener la longitud 3, sumamos 1 y shift: 110. Para obtener la siguiente palabra de longitud 3, sumamos 1: 111.

SímboloLa palabra clave
UNA0 0
si10
C110
re111

La implementación del generador de código canónico se muestra a continuación. Tenga en cuenta que el algoritmo Deflate espera que se generen palabras de código sobre la base del principio LSB-first (primero, el bit menos significativo). Es decir, el primer bit de la palabra de código debe almacenarse en el bit menos significativo. Esto significa que necesitamos cambiar el orden de los bits, por ejemplo, usando la tabla de búsqueda.

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

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

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

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

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

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

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

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

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

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

        return reversed >> (16 - n);
}

Ahora póngalo todo junto y escriba el código de inicialización del codificador:

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

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

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

También hacemos una función para configurar el codificador usando las longitudes de código ya calculadas:

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

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

Decodificación Huffman eficiente


La forma más fácil de decodificar a Huffman es atravesar el árbol comenzando desde la raíz, leyendo un bit de entrada a la vez y decidiendo qué rama tomar a continuación, izquierda o derecha. Cuando se alcanza un nodo hoja, es un carácter decodificado.

Este método a menudo se enseña en universidades y libros. Es simple y elegante, pero el procesamiento de un bit a la vez es demasiado lento. Es mucho más rápido decodificar usando la tabla de búsqueda. Para el ejemplo anterior, en el que la longitud máxima de la palabra de código es de tres bits, puede usar la siguiente tabla:

PedacitosSímboloLongitud de palabra de código
0 00UNA1
0 01UNA1
0 10UNA1
0 11UNA1
10 0si2
10 1si2
110C3
111re3

Aunque solo hay cuatro caracteres, necesitamos una tabla con ocho entradas para cubrir todas las combinaciones posibles de tres bits. Los símbolos con palabras de código de menos de tres bits tienen varias entradas en la tabla. Por ejemplo, la palabra 10 se "complementó" con 10 0 y 10 1 para cubrir todas las combinaciones de tres bits que comienzan con 10.

Para decodificar de esta manera, debe indexar en la tabla con los siguientes tres bits de entrada e inmediatamente encontrar el carácter correspondiente y la longitud de su palabra de código. La longitud es importante, porque a pesar de mirar los siguientes tres bits, necesitamos obtener el mismo número de bits de entrada que la longitud de la palabra de código.

El método basado en la tabla de búsqueda funciona muy rápidamente, pero tiene un inconveniente: el tamaño de la tabla se duplica con cada bit adicional en la longitud de la palabra de código. Es decir, la construcción de la tabla se ralentiza exponencialmente y, si deja de caber en la memoria caché del procesador, el método comienza a funcionar lentamente.

Debido a esto, la tabla de búsqueda generalmente se usa solo para palabras de código que no tienen una longitud mayor. Y para palabras más largas, tome un enfoque diferente. Así como la codificación de Huffman asigna palabras de código más cortas a caracteres más frecuentes, el uso de una tabla de búsqueda para palabras de código cortas es en muchos casos una excelente optimización.

En zlibSe utilizan varios niveles de tablas de búsqueda. Si la palabra de código es demasiado larga para la primera tabla, la búsqueda irá a la tabla secundaria para indexar los bits restantes.

Pero hay otro método muy elegante, basado en las propiedades de los códigos canónicos de Huffman. Se describe en la Implementación de códigos de prefijo de redundancia mínima (Moffat y Turpin, 1997), y también se explica en The Lost Huffman Paper de Charles Bloom.

Tomemos las palabras de código de la versión canónica: 0, 10, 110, 111. Realizaremos un seguimiento de las primeras palabras de código de cada longitud, así como el número de cada palabra de código en la secuencia general - "índice de símbolo".

Longitud de palabra de códigoPrimera palabra de códigoPrimer índice de caracteres
10 01 (A)
2102 (B)
31103 (C)

Dado que las palabras de código se asignan secuencialmente, si conocemos el número de bits, podemos encontrar en la tabla anterior el carácter que representan estos bits. Por ejemplo, para el 111 de tres bits, vemos que este es un desplazamiento de uno de la primera palabra de código de esta longitud (110). El primer índice de caracteres de esta longitud es 3, y un desplazamiento de uno nos da un índice de 4. Otra tabla compara el índice de caracteres con el carácter:

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

Una pequeña optimización: en lugar de almacenar por separado el primer índice de caracteres y la primera palabra de código, podemos almacenar el primer índice en la tabla menos la primera palabra de código:

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

Para comprender cuántos bits deben estimarse, nuevamente utilizamos la propiedad de secuencia de código. En nuestro ejemplo, todas las palabras de código válidas de un bit son estrictamente menos de 1, dos bits, estrictamente menos de 11, tres bits, menos de 1000 (de hecho, es cierto para todos los valores de tres bits). En otras palabras, la palabra de código de N bits válida debe ser estrictamente menor que la primera palabra de código de N bits más el número de palabras de código de N bits. Además, podemos desplazar estos límites a la izquierda para que sean de tres bits de ancho. Llamémoslo bits restrictivos para cada una de las longitudes de palabras de código:

Longitud de palabra de códigoBits límite
1100
2110
31000

El limitador para la longitud 3 se ha desbordado a 4 bits, pero esto solo significa que cualquier palabra de tres bits servirá.

Podemos buscar entre los datos de entrada de tres bits y compararlos con los bits restrictivos para comprender cuánto dura nuestra palabra de código. Después de la finalización, cambiamos los bits de entrada, solo para calcular su número correcto, y luego encontramos el índice de caracteres:

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

La complejidad temporal del proceso es lineal con respecto al número de bits en las palabras de código, pero el lugar se gasta de manera eficiente, solo se requiere cargar y comparar en cada paso, y dado que las palabras de código más cortas son más comunes, el método permite optimizar la compresión en muchas situaciones.

Código de decodificador completo:

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

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

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

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

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

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

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

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

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

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

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

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

        *num_used_bits = 0;
        return -1;
}

Para configurar el decodificador, calcularemos previamente los códigos canónicos, como para huffman_encoder_init , y completaremos diferentes tablas:

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

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

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

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

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

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

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

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

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

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

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

        return true;
}

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

        assert(len <= HUFFMAN_LOOKUP_TABLE_BITS);

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

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

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

Desinflar


El algoritmo de desinflado, introducido en PKZip 2.04c en 1993, es un método de compresión estándar en archivos Zip modernos. También se usa en gzip, PNG y muchos otros formatos. Utiliza una combinación de compresión LZ77 y codificación Huffman, que discutiremos e implementaremos en esta sección.

Antes de Deflate, PKZip usaba métodos de compresión Shrink, Reduce e Implode. Hoy son raros, aunque después de Deflate todavía estuvieron en uso por algún tiempo, porque consumieron menos memoria. Pero no los consideraremos.

Corrientes de bits


Deflate almacena las palabras de código de Huffman en un flujo de bits de acuerdo con el principio LSB-first. Esto significa que el primer bit de la secuencia se almacena en el bit menos significativo del primer byte.

Considere un flujo de bits (leer de izquierda a derecha) 1-0-0-1-1. Cuando se almacena de acuerdo con el principio LSB-first, el valor del byte se convierte en 0b00011001 (binario) o 0x19 (hexadecimal). Puede parecer que el flujo simplemente se representa al revés (en cierto sentido, lo es), pero la ventaja es que es más fácil para nosotros obtener los primeros N bits de una palabra de computadora: simplemente ocultamos los N bits menos significativos.

Estos procedimientos están tomados de bitstream.h :

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

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

Nuestro decodificador Huffman necesita mirar los siguientes bits en la secuencia (suficientes bits para la palabra de código más larga posible), y luego continuar la secuencia por la cantidad de bits utilizados por el símbolo decodificado:

#define ISTREAM_MIN_BITS (64 - 7)

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

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

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

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

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

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

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

La conclusión es que en las máquinas de 64 bits istream_bitsgeneralmente se puede ejecutar como una instrucción de arranque único y algo de aritmética, dado que los elementos de la estructura istream_testán en registros. read64le se implementa en bits.h (los compiladores modernos lo convierten en una sola descarga de 64 bits utilizando el principio little-endian):

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

También necesitamos una función para continuar el flujo de bits hasta el borde del siguiente byte:

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

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

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

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

        return byte;
}

Para un flujo de bits saliente, escribimos bits usando un proceso de lectura-modificación-escritura. En el caso rápido, puede escribir un bit usando una lectura de 64 bits, algún tipo de operación de bits y escritura de 64 bits.

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

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

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

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

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

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

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

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

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

        os->bitpos += n;

        return true;
}

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

También necesitamos escribir eficientemente bytes en la secuencia. Por supuesto, puede ejecutar repetidamente grabaciones de 8 bits, pero será mucho más rápido de usar memcpy:

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

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

        return true;
}

Desembalaje (inflación)


Dado que el algoritmo de compresión se llama desinflar (soplar, extraer aire de algo), el proceso de desembalaje a veces se llama inflación . Si primero estudia este proceso, entenderemos cómo funciona el formato. Puede ver el código en la primera parte de deflate.h y deflate.c , bits.h , tables.h y tables.c (generado usando generate_tables.c ).

Los datos comprimidos con Deflate se almacenan como una serie de bloques.. Cada bloque comienza con un encabezado de 3 bits, en el que se establece el primer bit (menos significativo) si este es el bloque final de la serie, y los otros dos bits indican su tipo.


Hay tres tipos de bloques: sin comprimir (0), comprimido mediante códigos fijos de Huffman (1) y comprimido mediante códigos "dinámicos" de Huffman (2).

Este código realiza el desempaquetado utilizando funciones auxiliares para diferentes tipos de bloques, que implementaremos más adelante:

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

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

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

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

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

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

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

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

        return HWINF_OK;
}

Bloques de desinflado sin comprimir


Estos son bloques "almacenados", el tipo más simple. Comienza con el siguiente límite de 8 bits del flujo de bits con una palabra de 16 bits (len) que indica la longitud del bloque. Esto es seguido por otra palabra de 16 bits (nlen), que complementa (se invierte el orden de los bits) de las palabras len. Se supone que nlen actúa como una simple suma de comprobación de len: si el archivo está dañado, entonces los valores probablemente no serán complementarios y el programa podrá detectar un error.


Después de len y nlen son datos sin comprimir. Dado que la longitud del bloque es un valor de 16 bits, el tamaño de los datos está limitado a 65.535 bytes.

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

        p = istream_byte_align(is);

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

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

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

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

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

        return HWINF_OK;
}

Desinfle los bloques utilizando códigos fijos de Huffman


Los bloques Deflate comprimidos usan el código Huffman para representar una secuencia de literales LZ77. Los vínculos de retroceso se rompen usando marcadores de final de bloque. Para literales, longitudes de backlink y marcadores, se usa el código litlen Huffman . Y para distancias de backlink, se usa el código dist .


Litlen codifica valores en el rango de 0-285. Los valores 0-255 se usan para bytes literales, 256 es el marcador de fin de bloque y 257-285 se usan para longitudes de enlace de retroceso.

Los vínculos de retroceso son de 3-258 bytes de longitud. El valor de Litlen determina la longitud base a la que se agregan cero o más bits adicionales de la secuencia para obtener la longitud completa de acuerdo con la tabla a continuación. Por ejemplo, un valor litlen de 269 significa una longitud base de 19 y dos bits adicionales. La adición de dos bits de la secuencia da la longitud final de 19 a 22.

LitlenBits extraLongitudes
2570 03
2580 04 4
2590 05 5
2600 06 6
2610 07 7
2620 08
2630 09 9
2640 010
265111-12
266113-14
267115-16
268117-18
269219-22
270223-26
271227-30
272231-34
273335-42
274343-50
275351-58
276359-66
2774 467-82
2784 483–98
2794 499-114
2804 4115-130
2815 5131-162
2825 5163-194
2835 5195–226
2845 5227–257
2850 0258

Tenga en cuenta que un valor litlen de 284 más 5 bits adicionales puede representar longitudes de 227 a 258, sin embargo, la especificación establece que la longitud 258, la longitud máxima del vínculo de retroceso, debe representarse utilizando un valor litlen separado. Se supone que esto reduce la codificación en situaciones donde a menudo se encuentra la longitud máxima.

El descompresor usa la tabla para obtener la longitud base y los bits adicionales del valor litlen (menos 257):

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

...

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

El código litlen fijo de Huffman es canónico y utiliza las siguientes longitudes de palabra de código (286–287 no son valores litlen válidos, pero están involucrados en la generación de código):

Valores de LitlenLongitud de palabra de código
0-1438
144-2559 9
256–2797 7
280–2878

El descompresor almacena estas longitudes en una tabla conveniente para la transmisión a huffman_decoder_init:

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

...

/* 287 */ 8,
};

Las distancias de enlace de retroceso varían de 1 a 32.768. Se codifican utilizando un esquema que es similar a un esquema de codificación de longitud. El código Huffman dist codifica valores de 0 a 29, cada uno de los cuales corresponde a la longitud base, a los que se agregan bits adicionales para obtener la distancia final:

DistBits extraDistancias
0 00 01
10 02
20 03
30 04 4
4 415-6
5 517-8
6 629-12
7 7213-16
8317-24
9 9325–32
104 433-48
once4 449-64
125 565-96
trece5 597-128
146 6129-192
quince6 6193–256
dieciséis7 7257–384
177 7385-512
Dieciocho8513-768
diecinueve8769-1024
veinte9 91025-1536
219 91537-2048
22102049-3072
23103073–4096
24once4097-6144
25once6145–8192
26128193–12288
271212289–16384
28trece16385–24576
29trece24577–32768

El código fijo de Huffman dist es canónico. Todas las palabras de código son de 5 bits de largo. Es simple, el descompresor almacena los códigos en una tabla que se puede usar con huffman_decoder_init(los valores dist 30–31 no son correctos. Se indica que están involucrados en la generación de códigos Huffman, pero en realidad no tienen ningún efecto):

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

...

/* 31 */ 5,
};

Código de descompresión o desempaquetado: desinfle el bloque usando códigos fijos de Huffman:

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

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

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

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

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

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

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

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

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

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

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

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

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

Preste atención a esta optimización: cuando no hay suficiente espacio en el búfer saliente, emitimos vínculos de retroceso utilizando la función a continuación, que copia 64 bits a la vez. Esto es "desordenado" en el sentido de que a menudo copia algunos bytes adicionales (hasta el siguiente múltiplo de 8). Pero funciona mucho más rápido lz77_output_backref, ya que requiere menos iteraciones cíclicas y accesos a la memoria. De hecho, los vínculos de retroceso cortos ahora se procesarán en una iteración, lo cual es muy bueno para predecir la ramificación.

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

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

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

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

Desinfle los bloques usando códigos dinámicos de Huffman


Los bloques desinflados que utilizan códigos dinámicos de Huffman funcionan de la misma manera que se describe anteriormente. Pero en lugar de los códigos predefinidos para litlen y dist, usan los códigos almacenados en el flujo Deflate al comienzo del bloque. El nombre probablemente no tenga éxito, ya que los códigos dinámicos de Huffman también se denominan códigos que cambian durante la codificación; esta es la codificación adaptativa de Huffman . Los códigos descritos aquí no tienen nada que ver con ese procedimiento. Son dinámicos solo en el sentido de que diferentes bloques pueden usar diferentes códigos.

Generar códigos dinámicos de litlen y dist es la parte más difícil del formato Deflate. Pero tan pronto como se generan los códigos, la descompresión se realiza de la misma manera que se describe en la parte anterior, usando 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);
}

Los códigos Litlen y dist para bloques Deflate dinámicos se almacenan como una serie de longitudes de palabras de código. Las longitudes mismas se codifican usando el tercer código de Huffman: codelen . Este código está determinado por la longitud de las palabras de código ( codelen_lens) que están almacenadas en el bloque (¿mencioné que es difícil?).


Al comienzo del bloque dinámico hay 14 bits que determinan el número de longitudes de palabras de código litlen, dist y codelen que se leerán desde el bloque:

#define MIN_CODELEN_LENS 4
#define MAX_CODELEN_LENS 19

#define MIN_LITLEN_LENS 257
#define MAX_LITLEN_LENS 288

#define MIN_DIST_LENS 1
#define MAX_DIST_LENS 32

#define CODELEN_MAX_LIT 15

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

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

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

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

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

        bits = istream_bits(is);

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

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

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

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

Luego vienen las longitudes de las palabras en código para el código codelen. Estas longitudes son los valores habituales de tres bits, pero están escritos en el orden especial especificado en codelen_lengths_order. Como es necesario determinar 19 longitudes, solo se leerá de la secuencia num_codelen_lens; todo lo demás es implícitamente nulo. Las longitudes se enumeran en un cierto orden, por lo que es más probable que las longitudes cero caigan al final de la lista y no se almacenen en el bloque.

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

Al configurar el decodificador de codelen, podemos leer las longitudes de las palabras de código litlen y dist de la secuencia.

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

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

16, 17 y 18 no son longitudes reales, son indicadores de que la longitud anterior debe repetirse varias veces, o que necesita repetir la longitud cero:

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

Tenga en cuenta que las longitudes litlen y dist se leen una por una en la matriz code_lengths. No se pueden leer por separado, porque las ejecuciones de longitud de código se pueden transferir desde las últimas longitudes de litlen a las primeras longitudes de dist.

Una vez preparadas las longitudes de las palabras en código, podemos configurar los decodificadores Huffman y volver a la tarea de decodificar literales y vínculos de retroceso:

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

Compresión (deflación)


En las partes anteriores, creamos todas las herramientas necesarias para la compresión Deflate: Lempel-Ziv, codificación Huffman, flujos de bits y una descripción de los tres tipos de bloques Deflate. Y en esta parte lo pondremos todo junto para obtener la compresión Deflate.

Compresión Lempel-Ziv analiza los datos de origen en una secuencia de vínculos de retroceso y literales. Esta secuencia debe dividirse y codificarse en bloques Deflate, como se describe en la parte anterior. Elegir un método de partición a menudo se llama bloqueo.. Por un lado, cada nuevo bloque significa algún tipo de sobrecarga, cuyo volumen depende del tipo de bloque y su contenido. Menos bloques, menos gastos generales. Por otro lado, estos costos de crear un nuevo bloque pueden dar sus frutos. Por ejemplo, si las características de los datos permiten una codificación Huffman más eficiente y reducen la cantidad total de datos generados.

El bloqueo es una tarea de optimización difícil. Algunos compresores (por ejemplo, Zopfli ) funcionan mejor que otros, pero la mayoría simplemente utiliza el enfoque codicioso: emiten bloques tan pronto como alcanzan un cierto tamaño.

Los diferentes tipos de bloques tienen sus propias restricciones de tamaño:

  • Los bloques sin comprimir no pueden contener más de 65.535 bytes.
  • Los códigos fijos de Huffman no tienen un tamaño máximo.
  • Los códigos dinámicos de Huffman generalmente no tienen un tamaño máximo, pero dado que nuestra implementación del algoritmo de Huffman usa secuencias de caracteres de 16 bits, estamos limitados a 65.535 caracteres.

Para usar bloques de cualquier tipo libremente, limite su tamaño a 65.534 bytes:

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

Para rastrear el flujo de bits saliente y el contenido del bloque actual durante la compresión, usaremos la estructura:

typedef struct deflate_state_t deflate_state_t;
struct deflate_state_t {
        ostream_t os;

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

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

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

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

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

Para agregar resultados de trabajo al bloque, lz77_compressutilizaremos las funciones de devolución de llamada y, al alcanzar el tamaño máximo, escribiremos el bloque en el flujo de bits:

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

Lo más interesante es la grabación de bloques. Si el bloque no está comprimido, entonces todo es simple:

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

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

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

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

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

        return true;
}

Para escribir un bloque estático de Huffman, primero generamos códigos canónicos basados ​​en longitudes de palabras de código fijas para los códigos litlen y dist. Luego iteramos el bloque, escribiendo los caracteres que usan estos códigos:

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

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

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

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

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

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

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

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

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

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

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

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

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

        return true;
}

Por supuesto, escribir bloques dinámicos de Huffman es más difícil porque contienen codificación complicada de códigos litlen y dist. Para representar esta codificación, utilizamos la siguiente estructura:

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

Primero, descartamos la cola de sus longitudes cero de las palabras de código litlen y dist, y luego las copiamos en una matriz regular para la codificación posterior. No podemos descartar todos los ceros: es imposible codificar un bloque Deflate si no hay un único código dist en él. También es imposible tener menos de 257 códigos litlen, pero como siempre tenemos un marcador de finalización de byte, siempre habrá una longitud de código distinta de cero para un carácter de 256.

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

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

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

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

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

        return encode_lens(lens, n, encoded);
}

Después de agregar las longitudes de código en una matriz, realizamos la codificación utilizando caracteres especiales para ejecutar las mismas longitudes de código.

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

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

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

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

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

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

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

        return num_encoded;
}

Los caracteres utilizados para la codificación se grabarán utilizando el código Huffman - codelen. Las longitudes de las palabras de código del código codelen se escriben en el bloque en un orden específico, de modo que es más probable que las longitudes cero terminen al final. Aquí hay una función que calcula cuántas longitudes se deben escribir:

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

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

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

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

        return n;
}

Supongamos que ya hemos establecido los códigos litlen y dist, configuramos la codificación de las longitudes de sus palabras de código y el código para estas longitudes. Ahora podemos escribir un bloque dinámico de Huffman:

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

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

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

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

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

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

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

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

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

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

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

        return write_huffman_block(s, litlen_enc, dist_enc);
}

Para cada bloque, queremos usar el tipo que requiere el menor número de bits. La longitud de un bloque sin comprimir se puede calcular rápidamente:

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

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

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

Para los bloques codificados de Huffman, puede calcular la longitud del cuerpo utilizando las frecuencias litlen y dist de caracteres y las longitudes de las palabras de código:

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

        len = 0;

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

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

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

        return len;
}

La longitud total del bloque estático es de 3 bits del encabezado más la longitud del cuerpo. Calcular el tamaño del encabezado de un bloque dinámico requiere mucho más trabajo:

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

        /* Block header. */
        len = 3;

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

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

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

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

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

Ahora juntaremos todo y crearemos la función principal para escribir bloques:

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

        old_bit_pos = ostream_bit_pos(&s->os);

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

        uncomp_len = uncomp_block_len(s);

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

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

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

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

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

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

        return true;
}

Finalmente, el iniciador de todo el proceso de compresión debe establecer el estado inicial, iniciar la compresión Lempel-Ziv y escribir el bloque resultante:

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

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

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

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

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

        *dst_used = ostream_bytes_written(&s.os);

        return true;
}

Formato de archivo zip


Arriba, examinamos cómo funciona la compresión Deflate utilizada en los archivos Zip. ¿Qué pasa con el formato de archivo en sí? En esta parte, examinaremos en detalle su estructura e implementación. El código está disponible en zip.h y zip.c .

Visión general


El formato del archivo se describe en la Nota de aplicación PKZip :

  1. Cada archivo, o elemento de archivo, en un archivo zip tiene un encabezado de archivo local con metadatos sobre el elemento.
  2. . , , , Zip-.
  3. , . , . Zip-.


Cada elemento del archivo se comprime y almacena individualmente. Esto significa que incluso si hay coincidencias entre archivos en el archivo, no se tendrán en cuenta para mejorar la compresión.

La ubicación del catálogo central al final le permite completar gradualmente el archivo. A medida que los elementos del archivo se comprimen, se agregan al archivo. El índice se registra después de todos los tamaños comprimidos, lo que le permite conocer las compensaciones de todos los archivos. Agregar archivos a un archivo existente es bastante fácil, se coloca después del último elemento y se sobrescribe el directorio central.

La capacidad de crear archivos gradualmente fue especialmente importante para diseminar información en numerosos disquetes o volúmenes. Mientras se comprimía, PKZip sugirió que los usuarios insertaran nuevos disquetes y escribieran el directorio central en los últimos (últimos). Para descomprimir un archivo de varios volúmenes, PKZip primero solicitó el último disquete para escribir para leer el directorio central, y luego el resto del disquete necesario para extraer los archivos solicitados.

Esto puede sorprenderlo, pero no había una regla que prohibiera tener múltiples archivos con el mismo nombre en el archivo. Esto podría generar mucha confusión al desempacar: si hay varios archivos con el mismo nombre, ¿cuál debería desempaquetar? A su vez, esto podría conducir a problemas de seguridad. Debido al error "Master Key" en Android (CVE-2013-4787 , diapositivas y video del informe sobre Black Hat) un atacante podría pasar por alto las comprobaciones del sistema operativo de firma criptográfica al instalar programas. Los programas de Android se distribuyen en archivos APK , que son archivos Zip. Al final resultó que, si el APK contenía varios archivos con el mismo nombre, el código de verificación de firma seleccionó el último archivo con el mismo nombre, y el instalador seleccionó el primer archivo, es decir, la verificación no se realizó. En otras palabras, esta pequeña diferencia entre las dos bibliotecas Zip hizo posible omitir todo el modelo de seguridad del sistema operativo.

A diferencia de la mayoría de los formatos, los archivos zip no deben comenzar con una firma o un número mágico. Por lo general, no se indica que el archivo Zip deba comenzar de ninguna manera específica, lo que le permite crear fácilmente archivos que son válidos como Zip y como un formato diferente: archivos políglotas . Por ejemplo, los archivos Zip autoextraíbles (por ejemplo, pkz204g.exe ) suelen ser tanto archivos ejecutables como Zip: la primera parte es ejecutable, seguida de un archivo Zip (que la parte ejecutable desempaqueta). El sistema operativo puede ejecutarlo como ejecutable, pero el programa Zip lo abrirá como un archivo Zip. Tal característica podría hacer que no se requiera una firma al comienzo del archivo.

Aunque estos archivos políglotas son inteligentes, pueden provocar problemas de seguridad porque pueden engañar a los programas que intentan determinar el contenido del archivo y también permiten entregar código malicioso en un lugar con archivos de diferentes tipos. Por ejemplo, los exploits utilizaron archivos GIFAR , que al mismo tiempo son imágenes GIF correctas y archivos Java (JAR, un tipo de archivo Zip). Para obtener más información sobre este problema, consulte el artículo Abuso de formatos de archivo (a partir de la página 18).

Como veremos a continuación, los archivos Zip usan campos de 32 bits para desplazamientos y tamaños para limitar el tamaño del archivo y sus elementos a cuatro gigabytes. En la nota de aplicación 4.5PKWare ha agregado extensiones de formato que permiten el uso de compensaciones y tamaños de 64 bits. Los archivos que usan estas extensiones están en formato Zip64, pero no los consideraremos.

Estructuras de datos


Fin de la entrada del directorio central


El final de una entrada de directorio central (EOCDR) se usa generalmente como punto de partida para leer un archivo zip. Contiene la ubicación y el tamaño del directorio central, así como comentarios opcionales sobre todo el archivo.

En los archivos zip que ocupaban varios disquetes, o volúmenes, el EOCDR también contenía información sobre qué disco estamos usando actualmente, en qué disco se inicia el directorio central, etc. Hoy, esta funcionalidad rara vez se usa, y el código de este artículo no procesa dichos archivos.

EOCDR está determinado por la firma 'P' 'K', seguido de los bytes 5 y 6. Le sigue la estructura a continuación, los números se almacenan de acuerdo con el principio little-endian:

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

EOCDR debe ubicarse al final del archivo. Pero dado que puede haber un comentario de longitud arbitraria de 16 bits en su cola, puede ser necesario encontrar una posición específica:

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

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

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

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

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

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

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

        return false;
}

Grabar EOCDR es fácil. Esta función escribe y devuelve el número de bytes escritos:

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

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

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

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

        return (size_t)(p - dst);
}

Encabezado de archivo central


El directorio central consta de encabezados de archivo central escritos uno tras otro, uno para cada elemento de archivo. Cada encabezado comienza con la firma 'P', 'K', 1, 2, y luego existe dicha estructura:

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

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

made_by_very extract_vercodificar información sobre el sistema operativo y la versión del programa utilizado para agregar este elemento, así como qué versión se necesita para recuperarlo. Los ocho bits más importantes codifican el sistema operativo (por ejemplo, 0 significa DOS, 3 significa Unix, 10 significa Windows NTFS), y los ocho bits inferiores codifican la versión del software. Establezca el valor decimal en 20, lo que significa compatibilidad con PKZip 2.0.

gp_flagContiene diferentes banderas. Estamos interesados:

  • Bit 0, que indica el hecho de cifrado del elemento (no consideraremos esto);
  • Y los bits 1 y 2, que codifican el nivel de compresión Deflate (0 - normal, 1 - máximo, 2 - rápido, 3 - muy rápido).

methodcodifica un método de compresión. 0 - datos no comprimidos, 8 - Delate aplicado. Otros valores se relacionan con algoritmos antiguos o nuevos, pero casi todos los Zip usan estos dos valores.

mod_timey mod_datecontienen la fecha y hora en que se modificó el archivo, codificado en formato MS-DOS . Usando este código, convertiremos las marcas time_tde tiempo C habituales hacia y desde el formato MS-DOS:

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

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

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

        tm.tm_isdst = -1;

        return mktime(&tm);
}

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

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

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

El campo crc32contiene el valor del código cíclico redundante de datos sin comprimir. Se utiliza para verificar la integridad de los datos después de la recuperación. Implementación aquí: crc32.c .

comp_sizey uncomp_sizecontienen el tamaño comprimido y sin comprimir de los datos del archivo del elemento. Los siguientes tres campos contienen la longitud del nombre, comentario y datos adicionales inmediatamente después del título. disk_nbr_startDiseñado para archivos que utilizan múltiples disquetes.

int_attrsy ext_attrsdescriba los atributos internos y externos del archivo. Los internos se relacionan con el contenido del archivo, por ejemplo, el bit menos significativo indica si el archivo contiene solo texto. Los atributos externos indican si el archivo está oculto, es de solo lectura, etc. El contenido de estos campos depende del sistema operativo, en particular, demade_by_ver. En DOS, los 8 bits inferiores contienen el byte de atributo de archivo, que se puede obtener de la llamada al sistema Int 21 / AX = 4300h . Por ejemplo, el bit 4 significa que es un directorio, y el bit 5 significa que el atributo "archivo" está establecido (verdadero para la mayoría de los archivos en DOS). Por lo que yo entiendo, en aras de la compatibilidad, estos bits se configuran de manera similar en otros sistemas operativos. En Unix, los 16 bits más altos de este campo contienen bits de modo de archivo que son devueltos por stat (2) en st_mode.

lfh_offsetnos dice dónde buscar el encabezado del archivo local. name- nombre de archivo (línea C), y comment- comentario opcional para este elemento de archivo (línea C). extrapuede contener datos adicionales opcionales, como información sobre el propietario del archivo Unix, fecha y hora más precisas del cambio o campos Zip64.

Esta función se usa para leer los encabezados centrales de los archivos:

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

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

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

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

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

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

        return true;
}

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

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

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

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

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

        return (size_t)(p - dst);
}

Encabezado de archivo local


Los datos de cada elemento de archivo están precedidos por un encabezado de archivo local, que repite la mayor parte de la información del encabezado central.

Es probable que se haya introducido la duplicación de datos en los encabezados central y local para que PKZip no mantenga todo el directorio central en la memoria al desempaquetar. En cambio, a medida que se extrae cada archivo, su nombre y otra información se pueden leer desde el encabezado local. Además, los encabezados locales son útiles para recuperar archivos de archivos Zip en los que falta o está dañado el directorio central.

Sin embargo, esta redundancia es también la principal fuente de incertidumbre. Por ejemplo, ¿qué sucede si los nombres de los archivos en los encabezados central y local no coinciden? Esto a menudo conduce a errores y problemas de seguridad.

No toda la información del encabezado central está duplicada. Por ejemplo, campos con atributos de archivo. Además, si se establece el tercer bit menos significativo gp_flags(CRC-32), los campos comprimidos y no comprimidos se restablecerán a cero, y esta información se puede encontrar en el bloque Descriptor de datos después de los datos del archivo en sí (no lo consideraremos). Esto le permite grabar un encabezado local antes de que se conozca el tamaño del archivo del elemento o de qué tamaño se comprimirá.

El encabezado local comienza con la firma 'P', 'K', 3, 4, y luego existe dicha estructura:

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

Estas funciones leen y escriben encabezados locales, como otras estructuras de datos:

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

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

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

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

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

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

        return true;
}

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

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

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

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

        return (size_t)(p - dst);
}

Implementación de Zip Read


Usando las funciones anteriores, implementamos la lectura del archivo Zip en la memoria y obtenemos un iterador para acceder a los elementos del archivo:

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

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

        const uint8_t *src;
        size_t src_len;
};

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

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

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

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

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

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

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

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

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

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

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

        zip->members_end = offset;

        return true;
}

Como mencioné anteriormente, los iteradores de elementos son simplemente desplazamientos al encabezado del archivo central a través del cual puede acceder a los datos del elemento:

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

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

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

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

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

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

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

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

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

        return m;
}

Implementación de registro postal


Para escribir un archivo zip en el búfer de memoria, primero debe averiguar cuánta memoria asignarle. Y dado que no sabemos cuántos datos comprimiremos antes de intentar escribir, calculamos el límite superior en función de los tamaños de los elementos sin comprimir:

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

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

        total = EOCDR_BASE_SZ + comment_len; /* EOCDR */

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

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

        if (total > UINT32_MAX) {
                return 0;
        }

        return (uint32_t)total;
}

Este código escribe el archivo zip usando la compresión desinflada de cada elemento, reduciendo su tamaño:

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

        p = dst;

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

                data_dst = p + LFH_BASE_SZ + name_len;

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

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

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

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

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

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

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

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

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

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

        return (uint32_t)(p - dst);
}

Hwzip


Ahora sabemos cómo leer y escribir archivos Zip, cómo comprimir y descomprimir los datos almacenados en ellos. Ahora escribamos un programa Zip simple que contenga todas estas herramientas. El código está disponible en hwzip.c .

Usaremos una macro para el manejo simple de errores y varias funciones auxiliares para la asignación de memoria comprobada:

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

Las otras dos funciones se usan para leer y escribir archivos:

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

Nuestro programa Zip puede realizar tres funciones: hacer una lista del contenido de los archivos Zip y extraerlo, así como crear archivos Zip. Listado no es más simple en ninguna parte:

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

La extracción es un poco más complicada. Utilizaremos funciones auxiliares para la terminación nula del nombre del archivo (para pasarlo fopen) y desempaquetar:

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

Nuestro programa omitirá cualquier elemento de archivo que tenga directorios. Esto se hace para evitar los llamados ataques transversales de ruta : se utiliza un archivo malicioso para escribir un archivo desde fuera del directorio especificado por el usuario. Lea los detalles en las preguntas frecuentes de 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);
}

Para crear un archivo zip, leeremos los archivos de entrada y los alimentaremos zip_write. Dado que la biblioteca estándar de C no le permite obtener la hora de modificación del archivo, usaremos la hora actual (lo dejo como tarea para corregir esta función).

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

Finalmente, mainverifica los argumentos de la línea de comando y decide qué hacer:

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

Instrucciones de montaje


Un conjunto completo de archivos fuente está disponible en hwzip-1.0.zip . Cómo compilar HWZip en Linux o 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

En Windows, en el símbolo del sistema del desarrollador en Visual Studio (si no tiene Visual Studio, descargue las herramientas de compilación ):

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 para expandir argumentos de línea de comando comodín .)

Conclusión


Es sorprendente cómo la tecnología se está desarrollando rápida y lentamente. El formato Zip fue creado hace 30 años basado en tecnología de los años cincuenta y setenta. Y aunque mucho ha cambiado desde entonces, los archivos Zip, de hecho, se han mantenido igual y hoy son más comunes que nunca. Creo que será útil tener una buena comprensión de cómo funcionan.

Ejercicios


  • Haga que HWZip registre el tiempo que tardó en cambiar cada archivo, en lugar de la hora actual en que se creó el archivo. Use stat (2) en Linux o Mac y GetFileTime en Windows. O agregue un indicador de línea de comando que permita al usuario establecer una hora específica para los cambios de archivo.
  • gzip-. — , Deflate ( ). RFC 1952.
  • Zip- . HWZip , read_file mmap(2) Linux Mac CreateFileMapping Windows.
  • HWZip , Zip64. appnote.txt.



All Articles