File Zip: Sejarah, Penjelasan, dan Implementasi



Saya sudah lama bertanya-tanya bagaimana data dikompresi, termasuk dalam file Zip. Suatu kali saya memutuskan untuk memuaskan rasa ingin tahu saya: untuk mempelajari cara kompresi, dan menulis program Zip saya sendiri. Implementasi telah menjadi latihan yang menyenangkan dalam pemrograman. Anda mendapatkan kesenangan luar biasa dari membuat mesin debugged yang mengambil data, mentransfer bit-bitnya ke representasi yang lebih efisien, dan kemudian mengumpulkannya kembali. Saya harap Anda juga akan tertarik membacanya.

Artikel ini menjelaskan dengan sangat rinci bagaimana file Zip dan skema kompresi bekerja: kompresi LZ77, algoritma Huffman, algoritma Deflate dan banyak lagi. Anda akan mempelajari sejarah perkembangan teknologi dan melihat contoh implementasi yang cukup efektif yang ditulis dari awal dalam C. Kode sumbernya ada di sini: hwzip-1.0.zip .

Saya sangat berterima kasih kepada Ange Albertini , Gynvael Coldwind , Fabian Giesen , Jonas Skeppstedt ( web ), Primiano Tucci, dan Nico Weber , yang memberikan umpan balik yang berharga pada draft artikel ini.

Kandungan



Cerita


PKZip


Pada tahun delapan puluhan dan awal sembilan puluhan, sebelum Internet menjadi luas, penggemar komputer menggunakan modem dial-up untuk terhubung melalui jaringan telepon ke jaringan Bulletin Board Systems (BBS). BBS adalah sistem komputer interaktif yang memungkinkan pengguna mengirim pesan, bermain game, dan berbagi file. Untuk online, cukup memiliki komputer, modem, dan nomor telepon BBS yang bagus. Nomor diterbitkan di majalah komputer dan di BBS lainnya.

Alat penting untuk memfasilitasi distribusi file adalah pengarsip . Ini memungkinkan Anda untuk menyimpan satu atau lebih file dalam satu file arsipuntuk lebih mudah menyimpan atau mengirimkan informasi. Dan idealnya, arsip juga mengkompresi file untuk menghemat ruang dan waktu untuk pengiriman melalui jaringan. Pada masa BBS, pengarsip Arc populer, ditulis oleh Tom Henderson dari System Enhancement Associates (SEA), sebuah perusahaan kecil yang ia dirikan bersama saudara iparnya.

Pada akhir 1980-an, programmer Phil Katz merilis Arc versi sendiri, PKArc. Itu kompatibel dengan Arc SEA, tetapi bekerja lebih cepat berkat subrutin yang ditulis dalam bahasa assembly dan menggunakan metode kompresi baru. Program ini menjadi populer, Katz berhenti dari pekerjaannya dan menciptakan PKWare untuk fokus pada pengembangan lebih lanjut. Menurut legenda, sebagian besar pekerjaan terjadi di dapur ibunya di Glendale, Wisconsin.


Foto oleh Phil Katz dari sebuah artikel di Milwaukee Sentinel , 19 September 1994.

Namun, KLHS tidak senang dengan inisiatif Katz. Perusahaan menuduhnya melakukan pelanggaran merek dagang dan hak cipta. Litigasi dan kontroversi di jaringan BBS dan dunia PC telah dikenal sebagai Arc Wars . Pada akhirnya, perselisihan diselesaikan untuk SEA.

Meninggalkan Arc, Katz menciptakan format pengarsipan baru pada tahun 1989, yang ia sebut Zip dan tersedia untuk umum :

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

Program Katz untuk membuat file seperti itu disebut PKZip dan segera menyebar ke dunia BBS dan PC.

Salah satu aspek yang paling mungkin berkontribusi pada keberhasilan format Zip adalah bahwa dokumentasi tersebut disertai dengan PKZip, Application Note , yang menjelaskan secara rinci bagaimana format itu bekerja. Ini memungkinkan orang lain untuk mempelajari format dan membuat program yang menghasilkan, mengekstrak, atau berinteraksi dengan file Zip.

Zip - format kompresi tanpa kehilangan : setelah mengekstrak data akan sama seperti sebelum kompresi. Algoritma mencari redundansi dalam sumber data dan lebih efisien menyajikan informasi. Pendekatan ini berbeda dari kompresi lossy ., yang digunakan dalam format seperti JPEG dan MP3: ketika dikompresi, beberapa informasi yang kurang terlihat oleh mata manusia atau telinga dibuang.

PKZip didistribusikan sebagai Shareware: dapat digunakan dan disalin secara bebas, tetapi penulis menyarankan pengguna untuk "mendaftar" program. Untuk $ 47, Anda bisa mendapatkan instruksi cetak, dukungan premium, dan versi aplikasi yang ditingkatkan.


Salah satu versi utama PKZip adalah 2.04c, dirilis pada 28 Desember 1992 ( versi 2.04g dirilis tidak lama setelah itu ). Ini menggunakan algoritma kompresi Deflate default. Versi menentukan pengembangan lebih lanjut dari kompresi dalam file Zip ( artikel yang ditujukan untuk rilis ).


Sejak itu, format zip telah digunakan dalam banyak format file lainnya. Misalnya, arsip Java (.jar), Paket Aplikasi Android (.apk), dan file .docx Microsoft Office menggunakan format Zip. Banyak format dan protokol menggunakan algoritma kompresi yang sama, Deflate. Katakanlah, halaman web mungkin ditransfer ke browser Anda sebagai file gzip, format yang menggunakan kompresi Deflate.

Phil Katz meninggal pada tahun 2000. PKWare masih ada dan mendukung format Zip, meskipun perusahaan berfokus terutama pada perangkat lunak perlindungan data.

Info-zip dan zlib


Tak lama setelah rilis PKZip pada tahun 1989, program lain untuk membongkar file Zip mulai muncul. Misalnya, program unzip yang dapat membongkar sistem Unix. Pada bulan Maret 1990, milis bernama Info-ZIP dibuat.

Grup Info-ZIP telah merilis program open source gratis unzip dan zip , yang digunakan untuk membongkar dan membuat file zip. Kode telah porting ke banyak sistem, dan itu masih standar untuk program Zip untuk sistem Unix. Ini kemudian membantu meningkatkan popularitas file Zip.

Setelah Info-ZIP kode yang melakukan kompresi dan dekompresi deflate dipindahkan ke perpustakaan zlib terpisah yang mereka tulisJean-loup Gailly (kompresi) dan Mark Adler (membongkar).


Jean-loup Gailly (kiri) dan Mark Adler (kanan) pada Penghargaan USENIX STUG 2009 .

Salah satu alasan untuk membuat perpustakaan adalah bahwa ia menyediakan kenyamanan menggunakan kompresi Deflate di aplikasi dan format lain, misalnya, di gzip dan PNG baru . Format baru ini dimaksudkan untuk menggantikan Compress dan GIF , yang menggunakan algoritma LZW yang dilindungi paten.

Sebagai bagian dari pembuatan format ini, Peter Deutsch menulis spesifikasi Deflate dan menerbitkannya dengan nama Internet RFC 1951 pada Mei 1996. Ini ternyata deskripsi yang lebih mudah diakses dibandingkan dengan Catatan Aplikasi PKZip asli.

Hari ini zlib digunakan di mana-mana. Mungkin dia sekarang bertanggung jawab untuk mengompresi halaman ini di server web dan membongkar di browser Anda. Saat ini, sebagian besar file zip dikompresi dan didekompresi menggunakan zlib.

Winzip


Banyak dari mereka yang tidak menemukan PKZip menggunakan WinZip. Pengguna PC beralih dari DOS ke Windows, dan dari PKZip ke WinZip.

Semuanya dimulai dengan proyek oleh programmer Nico Mac, yang menciptakan perangkat lunak untuk OS / 2 di Mansfield Software Group di Storrs-Mansfield, Connecticut. Nico menggunakan Presentation Manager, ini adalah antarmuka pengguna grafis di OS / 2, dan dia kesal karena dia harus beralih dari manajer file ke perintah DOS setiap kali dia ingin membuat file Zip.

Mac menulis sebuah program GUI sederhana yang bekerja dengan file Zip langsung di Presentation Manager, menamainya PMZip, dan merilisnya sebagai shareware pada 1990-an.

OS / 2 tidak berhasil, dan dunia PC mengambil alih Microsoft Windows. Pada tahun 1991, Mac memutuskan untuk belajar bagaimana menulis program Windows, dan proyek pertamanya adalah port aplikasi Zip-nya ke OS baru. Pada April 1991, WinZip 1.00 dirilis . Itu didistribusikan sebagai shareware dengan masa percobaan 21 hari dan biaya pendaftaran $ 29. Dia terlihat seperti ini:


Dalam versi pertama WinZip, PKZip digunakan di bawah tenda. Tetapi dari versi 5.0 pada tahun 1993, kode dari Info-ZIP mulai digunakan untuk pemrosesan langsung file Zip. Antarmuka pengguna juga berevolusi secara bertahap.


WinZip 6.3 di bawah Windows 3.11 untuk Workgroups.

WinZip adalah salah satu program shareware paling populer di tahun 1990-an. Tetapi pada akhirnya, itu kehilangan relevansi karena menanamkan dukungan untuk file Zip dalam sistem operasi. Windows telah bekerja dengan mereka sebagai "folder terkompresi" sejak tahun 2001 (Windows XP), perpustakaan DynaZip digunakan untuk ini.

Mac pada awalnya bernama Nico Mak Computing. Pada tahun 2000, namanya diganti menjadi WinZip Computing, dan sekitar tahun-tahun itu Mack meninggalkannya. Pada tahun 2005, Vector Capital menjual perusahaan, dan pada akhirnya, itu menjadi milik Corel , yang masih merilis WinZip sebagai produk.

Kompresi Lempel-Ziv (LZ77)


Kompresi zip terdiri dari dua bahan utama: kompresi Lempel-Ziv dan kode Huffman.

Salah satu cara untuk mengompres teks adalah dengan membuat daftar kata atau frasa umum dengan penggantian varietas kata-kata ini di dalam teks dengan tautan ke kamus. Misalnya, kata panjang "kompresi" dalam teks sumber dapat direpresentasikan sebagai # 1234, di mana 1234 merujuk pada posisi kata dalam daftar. Ini disebut kompresi kamus .

Tetapi dari sudut pandang kompresi universal, metode ini memiliki beberapa kelemahan. Pertama, apa sebenarnya yang harus masuk ke kamus? Sumber data dapat dalam berbagai bahasa, bahkan bisa juga bukan teks yang dapat dibaca manusia. Dan jika kamus tidak disetujui sebelumnya antara kompresi dan dekompresi, maka kamus harus disimpan dan ditransfer bersama dengan data terkompresi, yang mengurangi manfaat kompresi.

Solusi elegan untuk masalah ini adalah dengan menggunakan sumber data itu sendiri sebagai kamus. Pada tahun 1977, Algoritma Universal untuk Kompresi Data Sekuensial , Jacob Ziv dan Abraham Lempel (yang bekerja di Technion), mengusulkan skema kompresi di mana data sumber disajikan sebagai urutan kembar tiga:

(, , )

di mana mereka membentuk tautan mundur ke urutan karakter yang ingin Anda salin dari posisi sebelumnya di teks asli, dan ini adalah karakter berikutnya dalam data yang dihasilkan.


Abraham Lempel dan Jacob Ziv.

Pertimbangkan baris berikut:

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

Pada baris kedua, urutan "t was the w" dapat direpresentasikan sebagai (26, 10, w), karena dibuat kembali dengan menyalin 10 karakter dari posisi 26 karakter kembali ke huruf "w". Untuk karakter yang belum muncul, backlink panjang nol digunakan. Misalnya, "I" awal dapat direpresentasikan sebagai (0, 0, I).

Skema ini disebut kompresi Lempel-Ziv, atau kompresi LZ77. Namun, dalam implementasi praktis dari algoritma, bagian dari triplet biasanya tidak digunakan . Sebagai gantinya, karakter dihasilkan secara individual, dan ( , ) pasangan digunakan untuk backlink (opsi ini disebut kompresi LZSS ). Bagaimana literal dan backlink dikodekan adalah masalah yang terpisah, kami akan mempertimbangkannya di bawah ini ketika kami menganalisis algoritmaMengempis .

Teks ini:

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

Anda dapat mengompres ini:

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

Salah satu sifat penting dari backlink adalah mereka dapat tumpang tindih. Ini terjadi ketika panjangnya lebih besar dari jarak. Contohnya:

Fa-la-la-la-la

Anda dapat mengompres ke:

Fa-la(3,9)

Ini mungkin tampak aneh bagi Anda, tetapi metode ini bekerja: setelah byte dari tiga "-la" pertama disalin, penyalinan terus menggunakan byte yang baru dibuat.

Bahkan, ini adalah jenis pengkodean panjang seri , di mana bagian data disalin berulang kali untuk mendapatkan panjang yang diinginkan.

Contoh interaktif menggunakan kompresi Lempel-Ziv untuk lirik ditampilkan dalam artikel oleh Colin Morris. Apakah Pop Lyrics Getting More Repetitive? .

Berikut ini adalah contoh menyalin backlink di C. Harap dicatat bahwa karena kemungkinan tumpang tindih, kami tidak dapat menggunakan memcpyatau 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++;
        }
}

Mudah untuk menghasilkan literal, tetapi untuk kelengkapannya kita akan menggunakan fungsi bantu:

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

Perhatikan bahwa pemanggil fungsi ini harus memastikan bahwa ada dstcukup ruang untuk data yang dihasilkan dan bahwa backlink tidak mengakses posisi sebelum dimulainya buffer.

Sulit untuk tidak menghasilkan data menggunakan backlink selama membongkar, tetapi untuk membuatnya terlebih dahulu saat mengompresi sumber data. Ini dapat dilakukan dengan cara yang berbeda, tetapi kami akan menggunakan metode berdasarkan tabel hash dari zlib, yang diusulkan dalam RFC 1951.

Kami akan menggunakan tabel hash dengan posisi awalan tiga karakter yang sebelumnya ditemukan dalam baris (backlink yang lebih pendek tidak membawa manfaat apa pun). Deflate memungkinkan backlink dalam 32.768 karakter sebelumnya - ini disebut jendela. Ini memberikan kompresi streaming: data input diproses sedikit demi sedikit, asalkan jendela dengan byte terakhir disimpan dalam memori. Namun, implementasi kami mengasumsikan bahwa semua data input tersedia untuk kami dan bahwa kami dapat memprosesnya secara bersamaan. Ini memungkinkan Anda untuk fokus pada kompresi daripada akuntansi, yang diperlukan untuk pemrosesan aliran.

Kami akan menggunakan dua array: di headberisi nilai hash dari tiga karakter awalan untuk posisi di input, dan di prevberisi posisi posisi sebelumnya dengan nilai hash ini. Bahkan, head[h]ini adalah judul dari daftar posisi awalan yang ditautkan dengan hash h, dan prev[x]menerima elemen sebelum xdaftar.

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

Untuk memasukkan posisi string baru ke tabel hash prev, itu diperbarui untuk menunjukkan yang sebelumnya head, dan kemudian diperbarui sendiri 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;
}

Perhatikan operasi modulo saat mengindeks prev: kami hanya tertarik pada posisi yang termasuk dalam jendela saat ini.

Alih-alih menghitung nilai hash untuk setiap awalan tiga karakter dari awal, kami akan menggunakan hash ring dan akan terus memperbaruinya sehingga hanya tiga karakter terakhir yang tercermin dalam nilainya:

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

Peta hash kemudian dapat digunakan untuk secara efisien mencari kecocokan sebelumnya dengan urutan, seperti yang ditunjukkan di bawah ini. Mencari kecocokan adalah operasi kompresi paling intensif sumber daya, jadi kami akan membatasi kedalaman pencarian dalam daftar.

Mengubah berbagai parameter, seperti kedalaman pencarian pada daftar awalan dan melakukan perbandingan malas, seperti dijelaskan di bawah, adalah cara untuk meningkatkan kecepatan dengan mengurangi tingkat kompresi. Pengaturan dalam kode kami dipilih agar sesuai dengan tingkat kompresi maksimum di 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;
}

Anda dapat menghentikan fungsi dengan lz77_compresskode ini untuk mencari kecocokan sebelumnya:

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

Kode ini mencari backlink terpanjang yang dapat dihasilkan pada posisi saat ini. Tetapi sebelum mengeluarkannya, program memutuskan apakah mungkin untuk menemukan kecocokan yang lebih lama di posisi berikutnya. Dalam zlib, ini disebut evaluasi perbandingan malas . Ini masih merupakan algoritma serakah : ia memilih pertandingan yang paling lama, bahkan jika yang lebih pendek saat ini memungkinkan Anda untuk kemudian mendapatkan pertandingan yang lebih lama dan mencapai kompresi yang lebih kuat.

Kompresi Lempel-Ziv dapat bekerja cepat dan lambat. Zopflimenghabiskan banyak waktu mencari backlink optimal untuk memeras persentase kompresi tambahan. Ini berguna untuk data yang dikompresi sekali dan kemudian digunakan kembali, misalnya, untuk informasi statis di server web. Di sisi lain skala adalah kompresor seperti Snappy dan LZ4 , yang dibandingkan hanya dengan awalan 4-byte terakhir dan sangat cepat. Jenis kompresi ini berguna dalam database dan sistem RPC di mana waktu yang dihabiskan untuk kompresi terbayar dengan menghemat waktu saat mengirim data melalui jaringan atau ke disk.

Gagasan menggunakan data sumber sebagai kamus sangat elegan, tetapi Anda juga dapat memanfaatkan kamus statis. Brotli adalah algoritma berbasis LZ77, tetapi juga menggunakan yang besarkamus statis string, yang sering ditemukan di jaringan.

Kode LZ77 dapat dilihat di lz77.h dan lz77.c .

Kode Huffman


Algoritma kompresi Zip kedua adalah kode Huffman. Kode

istilah dalam konteks ini adalah referensi ke sistem untuk menyajikan data dalam bentuk lain. Dalam hal ini, kami tertarik pada kode yang dapat digunakan untuk secara efisien mewakili literal dan backlink yang dihasilkan oleh algoritma Lempel-Ziv. Secara tradisional, teks bahasa Inggris disajikan menggunakan American Standard Code for Information Interchange (ASCII) . Sistem ini memberikan setiap karakter nomor, yang biasanya disimpan dalam representasi 8-bit. Berikut adalah kode ASCII untuk huruf besar alfabet bahasa Inggris:



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

Satu byte per karakter adalah cara mudah untuk menyimpan teks. Ini memungkinkan Anda untuk dengan mudah mengakses atau memodifikasi bagian teks, dan selalu jelas berapa banyak byte yang diperlukan untuk menyimpan N karakter, atau berapa banyak karakter yang disimpan dalam N byte. Namun, ini bukan cara yang paling efektif dalam hal ruang yang ditempati. Misalnya, dalam bahasa Inggris, huruf E paling sering digunakan, dan Z paling sedikit digunakan. Oleh karena itu, dalam hal volume, lebih efisien untuk menggunakan representasi bit yang lebih pendek untuk E dan yang lebih lama untuk Z, daripada menetapkan jumlah bit yang sama untuk setiap karakter.

Kode yang menentukan pengkodean panjang yang berbeda untuk karakter sumber yang berbeda disebut kode panjang variabel . Contoh paling terkenal adalah kode Morse., di mana setiap karakter dikodekan dengan titik dan garis, awalnya ditransmisikan dengan telegraf dengan pulsa pendek dan panjang:

SEBUAHโ€ข -N- โ€ข
B- โ€ข โ€ข โ€ขHAI- - -
C- โ€ข - โ€ขPโ€ข - - โ€ข
D- โ€ข โ€ขQ- - โ€ข -
Eโ€ขRโ€ข - โ€ข
Fโ€ข โ€ข - โ€ขSโ€ข โ€ข โ€ข
G- - โ€ขT-
Hโ€ข โ€ข โ€ข โ€ขUโ€ข โ€ข -
sayaโ€ข โ€ขVโ€ข โ€ข โ€ข -
Jโ€ข - - -Wโ€ข - -
K- โ€ข -X- โ€ข โ€ข -
L.โ€ข - โ€ข โ€ขY- โ€ข - -
M.- -Z- - โ€ข โ€ข

Salah satu kelemahan dari kode Morse adalah bahwa satu codeword mungkin merupakan awalan dari yang lain. Misalnya, โ€ข โ€ข - โ€ข tidak memiliki decoding yang unik: itu bisa F atau ER. Ini diselesaikan dengan jeda (panjang tiga titik) antara huruf-huruf selama transmisi. Namun, akan lebih baik jika codeword tidak bisa menjadi awalan dari kata lain. Kode ini disebut tidak direvisi . Kode ASCII dengan panjang tetap tidak diperbaiki karena codeword selalu memiliki panjang yang sama. Tetapi kode panjang variabel juga bisa diperbaiki. Nomor telepon sering tidak diperbaiki. Sebelum nomor telepon darurat 112 diperkenalkan di Swedia, semua nomor yang dimulai dengan 112 harus diubah Dan di AS tidak ada nomor telepon tunggal yang dimulai dengan 911.

Untuk meminimalkan ukuran pesan yang disandikan, lebih baik menggunakan kode yang tidak diperbaiki di mana karakter yang sering muncul memiliki kata sandi yang lebih pendek. Kode optimal adalah kode yang menghasilkan hasil sesingkat mungkin - jumlah panjang kata-kata kode, dikalikan dengan frekuensi kemunculannya, akan menjadi jumlah minimum yang mungkin. Ini disebut kode non-awalan dengan redundansi minimal , atau kode Huffman , untuk menghormati penemu algoritma yang efisien untuk menghasilkan kode tersebut.

Algoritma Huffman


Saat mempelajari bahan-bahan untuk menulis disertasi doktoralnya mengenai teknik elektronik di MIT, David Huffman mengikuti kursus teori informasi yang diajarkan oleh Robert Fano. Menurut legenda , Fano mengizinkan murid-muridnya untuk memilih: menulis ujian akhir atau kursus. Huffman memilih yang terakhir, dan dia diberi topik mencari kode awalan dengan redundansi minimal. Diasumsikan bahwa dia tidak tahu bahwa Fano sendiri sedang mengerjakan tugas ini pada waktu itu ( algoritma Shannon-Fano adalah metode yang paling terkenal pada tahun-tahun itu ). Karya Huffman diterbitkan pada tahun 1952 dengan judul A Method for Construction of Minimum-Redundancy Codes pada tahun 1952. Dan sejak itu algoritmanya telah banyak digunakan.


Siaran pers David Huffman, UC Santa Cruz.

Algoritma Huffman membuat kode yang tidak diperbaiki dengan redundansi minimal untuk rangkaian karakter dan frekuensi penggunaannya. Algoritme berulang kali memilih dua karakter yang paling tidak mungkin ditemukan dalam sumber data - katakanlah, X dan Y - dan menggantinya dengan karakter komposit yang berarti "X atau Y". Frekuensi kemunculan simbol gabungan adalah jumlah dari frekuensi dua simbol sumber. Codeword untuk X dan Y dapat berupa codeword apa saja yang ditugaskan ke karakter majemuk "X atau Y" diikuti oleh 0 atau 1 untuk membedakan antara karakter asli. Ketika input data direduksi menjadi satu karakter, algoritma berhenti bekerja ( penjelasan video ).

Berikut adalah contoh algoritme yang bekerja pada rangkaian karakter kecil:

SimbolFrekuensi
SEBUAH6
B4
C2
D3

Iterasi pengolahan pertama:


Dua simbol paling langka, C dan D, dihapus dari himpunan dan digantikan oleh simbol gabungan yang frekuensinya adalah jumlah dari frekuensi C dan D:


Sekarang simbol paling langka adalah B dan simbol komposit dengan frekuensi 5. Mereka dihapus dari himpunan dan diganti dengan simbol komposit dengan frekuensi 9:


Akhirnya, A dan simbol gabungan dengan frekuensi 9 digabungkan menjadi simbol baru dengan frekuensi 15:


Seluruh set dikurangi menjadi satu karakter, pemrosesan selesai.

Algoritma menciptakan struktur yang disebut pohon Huffman . Karakter input adalah daun, dan semakin tinggi frekuensi karakter, semakin tinggi lokasinya. Mulai dari akar pohon, Anda dapat membuat kata-kata kode untuk karakter dengan menambahkan 0 atau 1 ketika masing-masing bergerak ke kiri atau kanan. Ternyata seperti ini:

SimbolKata sandi
SEBUAH0
B10
C110
D111

Tidak ada codeword yang merupakan awalan untuk yang lain. Semakin sering simbol muncul, semakin pendek kata kodenya.

Tree juga dapat digunakan untuk decoding: kita mulai dari root dan ke kanan atau kiri untuk nilai dengan 0 atau 1 di depan karakter. Misalnya, baris 010100 diterjemahkan dalam ABBA.

Perhatikan bahwa panjang setiap codeword setara dengan kedalaman node tree yang sesuai. Seperti yang akan kita lihat di bagian selanjutnya, kita tidak perlu pohon asli untuk menetapkan codeword. Cukup mengetahui panjang kata-kata itu sendiri. Dengan demikian, hasil implementasi kami dari algoritma Huffman akan menjadi panjang kata-kata kode.

Untuk menyimpan set karakter dan secara efisien menemukan frekuensi terendah, kami akan menggunakan struktur data tumpukan biner . Secara khusus, kami tertarikmin-heap , karena nilai minimum harus di atas.

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

Untuk melacak frekuensi nkarakter, kami akan menggunakan banyak nelemen. Juga, setiap kali simbol komposit dibuat, kami ingin "menautkan" kedua simbol sumber itu. Oleh karena itu, setiap simbol akan memiliki "elemen komunikasi".

Untuk menyimpan nheap- nelemen dan elemen komunikasi, kita akan menggunakan array n * 2 + 1elemen. Ketika dua karakter pada heap digantikan oleh satu, kita akan menggunakan elemen kedua untuk menyimpan tautan ke karakter baru. Pendekatan ini didasarkan pada implementasi Mengelola Gigabita Witten, Moffat dan Bell.

Pada setiap node di heap, kita akan menggunakan 16 bit paling signifikan untuk menyimpan frekuensi simbol, dan 16 bit lebih rendah untuk menyimpan indeks elemen komunikasi simbol. Karena penggunaan bit tinggi, perbedaan frekuensi akan ditentukan oleh hasil perbandingan 32-bit antara dua elemen heap.

Karena representasi ini, kita perlu memastikan bahwa frekuensi karakter selalu cocok dalam 16 bit. Setelah menyelesaikan algoritma, simbol komposit akhir akan memiliki frekuensi semua simbol gabungan, yaitu, jumlah ini harus ditempatkan dalam 16 bit. Implementasi Deflate kami akan memverifikasi ini dengan secara simultan memproses hingga 64.535 karakter.

Simbol dengan frekuensi nol akan menerima kata-kata kode dengan panjang nol dan tidak akan berpartisipasi dalam kompilasi pengkodean.

Jika kata kode mencapai kedalaman maksimum yang ditentukan, kami akan โ€œmemperlancarโ€ distribusi frekuensi dengan memberlakukan batas frekuensi dan mencoba lagi (ya, dengan bantuan goto). Ada cara yang lebih canggih untuk melakukan pengkodean Huffman yang terbatas, tetapi yang ini sederhana dan efisien.

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

Alternatif elegan untuk opsi tumpukan biner adalah menyimpan karakter dalam dua antrian. Yang pertama berisi karakter sumber, diurutkan berdasarkan frekuensi. Ketika simbol gabungan dibuat, ditambahkan kedua. Dengan demikian, simbol dengan frekuensi terendah akan selalu berada di posisi pertama dari salah satu antrian. Pendekatan ini dijelaskan oleh Jan van Leeuwen dalam On the Construction of Huffman Trees (1976).

Pengodean Huffman optimal untuk kode non-awalan, tetapi dalam kasus lain ada metode yang lebih efisien: pengkodean aritmatika dan sistem bilangan asimetris .

Kode kanonik Huffman


Pada contoh di atas, kami membangun pohon Huffman:


Jika kita pergi dari root dan menggunakan 0 untuk cabang kiri dan 1 untuk kanan, maka kita mendapatkan kode berikut:

SimbolKata sandi
SEBUAH0
B10
C110
D111

Keputusan untuk menggunakan 0 untuk cabang kiri dan 1 untuk yang benar tampaknya sewenang-wenang. Jika kita melakukan yang sebaliknya, kita mendapatkan:

SimbolKata sandi
SEBUAH1
B01
C001
D000

Kita dapat secara acak menandai dua cabang yang berasal dari simpul dengan nol dan satu (yang utama adalah bahwa labelnya berbeda), dan masih mendapatkan kode yang setara:


SimbolKata sandi
SEBUAH0
Bsebelas
C100
D101

Meskipun algoritma Huffman memberikan panjang kode sandi yang diperlukan untuk kode yang tidak diperbaiki dengan redundansi minimal, ada banyak cara untuk menetapkan masing-masing kode kata.

Mengingat panjang codeword yang dihitung oleh algoritma Huffman, kode Huffman kanonik memberikan codeword ke karakter dengan cara tertentu. Ini berguna karena memungkinkan Anda untuk menyimpan dan mengirimkan panjang codeword dengan data terkompresi: decoder akan dapat memulihkan codeword berdasarkan panjangnya. Tentu saja, Anda dapat menyimpan dan mengirimkan frekuensi simbol dan menjalankan algoritma Huffman di dekoder, tetapi ini akan membutuhkan lebih banyak pekerjaan dan lebih banyak penyimpanan dari dekoder. Properti lain yang sangat penting adalah bahwa struktur kode kanonik menggunakan decoding yang efisien.

Idenya adalah untuk menetapkan kata kode ke karakter secara berurutan, di bawah satu per satu. Kata kode pertama adalah 0. Berikutnya akan menjadi kata dengan panjang kata sebelumnya + 1. Kata pertama dengan panjang N terdiri dari kata terakhir panjang N-1, menambahkan satu (untuk mendapatkan kata kode baru) dan menggeser satu langkah ke kiri (untuk menambah panjang).

Dalam terminologi pohon Hoffman, kata-kata kode secara berurutan ditugaskan untuk meninggalkan dalam urutan dari kiri ke kanan, satu tingkat pada satu waktu, bergeser ke kiri ketika pindah ke tingkat berikutnya.

Dalam contoh ABCD kami, algoritma Huffman menetapkan kata-kata kode dengan panjang 1, 2, 3, dan 3. Kata pertama adalah 0. Ini juga kata terakhir dari panjang 1. Untuk panjang 2, kita mengambil 0 dan menambahkan 1 untuk mendapatkan kode berikutnya, yang akan menjadi awalan dari kode dua-bit , geser ke kiri dan dapatkan 10. Ini sekarang kata terakhir dari panjang 2. Untuk mendapatkan panjang 3, kita tambahkan 1 dan bergeser: 110. Untuk mendapatkan kata panjang berikutnya 3 kita tambahkan 1: 111.

SimbolKata sandi
SEBUAH0
B10
C110
D111

Implementasi generator kode kanonik ditunjukkan di bawah ini. Perhatikan bahwa algoritma Deflate mengharapkan codeword dihasilkan berdasarkan prinsip LSB-first (pertama, bit paling tidak signifikan). Artinya, bit pertama dari codeword harus disimpan dalam bit yang paling tidak signifikan. Ini berarti bahwa kita perlu mengubah urutan bit, misalnya, menggunakan tabel pencarian.

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

Sekarang kumpulkan semuanya dan tulis kode inisialisasi encoder:

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

Kami juga membuat fungsi untuk mengonfigurasi pembuat enkode menggunakan panjang kode yang sudah dihitung:

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

Decoding Huffman yang Efisien


Cara termudah untuk memecahkan kode Huffman adalah dengan melintasi pohon mulai dari root, membaca sedikit input sekaligus dan memutuskan cabang mana yang akan diambil selanjutnya, kiri atau kanan. Ketika simpul daun tercapai, itu adalah karakter yang diterjemahkan.

Metode ini sering diajarkan di universitas dan buku. Ini sederhana dan elegan, tetapi memproses sedikit demi sedikit terlalu lambat. Jauh lebih cepat untuk memecahkan kode menggunakan tabel pencarian. Untuk contoh di atas, di mana panjang codeword maksimum adalah tiga bit, Anda dapat menggunakan tabel berikut:

BitsSimbolPanjang Codeword
0 00SEBUAH1
0 01SEBUAH1
0 10SEBUAH1
0 11SEBUAH1
10 0B2
10 1B2
110C3
111D3

Meskipun hanya ada empat karakter, kita membutuhkan tabel dengan delapan entri untuk mencakup semua kemungkinan kombinasi tiga-bit. Simbol dengan codeword yang lebih pendek dari tiga bit memiliki beberapa entri dalam tabel. Misalnya, kata 10 "ditambah" dengan 10 0 dan 10 1 untuk mencakup semua kombinasi tiga bit dimulai dengan 10.

Untuk memecahkan kode dengan cara ini, Anda perlu mengindeks dalam tabel dengan tiga bit input berikut dan segera menemukan karakter yang sesuai dan panjang kata kodenya. Panjangnya penting, karena meskipun melihat pada tiga bit berikutnya, kita perlu mendapatkan jumlah bit input yang sama dengan panjang codeword.

Metode pencarian berdasarkan tabel bekerja sangat cepat, tetapi memiliki kelemahan: ukuran tabel berlipat ganda dengan setiap bit tambahan dalam panjang kata sandi. Artinya, konstruksi tabel melambat secara eksponensial, dan jika tidak lagi sesuai dengan cache prosesor, maka metode mulai bekerja lambat.

Karena itu, tabel pencarian biasanya hanya digunakan untuk kata-kata kode yang tidak lebih besar dari panjang tertentu. Dan untuk kata-kata yang lebih panjang, ambil pendekatan yang berbeda. Sama seperti pengkodean Huffman yang memberikan codeword yang lebih pendek ke karakter yang lebih sering, penggunaan tabel pencarian untuk codeword pendek dalam banyak kasus merupakan optimasi yang sangat baik.

Dalam zlibbeberapa level tabel pencarian digunakan. Jika kata sandi terlalu panjang untuk tabel pertama, maka pencarian akan pergi ke tabel sekunder untuk mengindeks bit yang tersisa.

Tetapi ada metode lain yang sangat elegan, berdasarkan pada sifat-sifat kode Huffman kanonik. Hal ini dijelaskan dalam Tentang Implementasi Kode Awalan Redundansi Minimum (Moffat dan Turpin, 1997), dan juga dijelaskan dalam The Lost Huffman Paper karya Charles Bloom.

Ambil kata-kata kode dari versi kanonik: 0, 10, 110, 111. Kami akan melacak kata-kata kode pertama dari setiap panjang, serta jumlah setiap kata kode dalam urutan umum - "indeks simbol".

Panjang CodewordCodeword pertamaIndeks karakter pertama
101 (A)
2102 (B)
31103 (C)

Karena codeword ditugaskan secara berurutan, jika kita mengetahui jumlah bit, kita dapat menemukan dalam tabel di atas karakter yang diwakili bit-bit ini. Sebagai contoh, untuk three-bit 111 kita melihat bahwa ini adalah offset oleh salah satu dari codeword pertama panjang ini (110). Indeks karakter pertama dari panjang ini adalah 3, dan offset satu memberi kita indeks 4. Tabel lain membandingkan indeks karakter dengan karakter:

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

Sedikit optimisasi: alih-alih secara terpisah menyimpan indeks karakter pertama dan codeword pertama, kita dapat menyimpan indeks pertama dikurangi codeword pertama dalam tabel:

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

Untuk memahami berapa banyak bit yang perlu diperkirakan, kami kembali menggunakan properti urutan kode. Dalam contoh kami, semua codeword satu-bit yang valid benar-benar kurang dari 1, dua-bit - benar-benar kurang dari 11, tiga-bit - kurang dari 1000 (pada kenyataannya, berlaku untuk semua nilai tiga-bit). Dengan kata lain, codeword N-bit yang valid harus benar-benar kurang dari codeword N-bit pertama ditambah jumlah codeword N-bit. Selain itu, kita dapat menggeser batas-batas ini ke kiri sehingga lebar mereka tiga bit. Sebut saja bit restriktif untuk masing-masing panjang codeword:

Panjang CodewordBatasi bit
1100
2110
31000

Pembatas untuk panjang 3 telah meluap menjadi 4 bit, tetapi ini hanya berarti bahwa setiap kata tiga bit akan dilakukan.

Kita dapat mencari di antara data input tiga bit dan membandingkannya dengan bit terbatas untuk memahami berapa lama kata kode kita. Setelah selesai, kami menggeser bit input, hanya untuk menghitung angka yang benar, dan kemudian menemukan indeks karakter:

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

Kompleksitas waktu dari proses adalah linear sehubungan dengan jumlah bit dalam kata-kata kode, tetapi tempat dihabiskan secara efisien, hanya memuat dan perbandingan diperlukan pada setiap langkah, dan karena kata-kata kode yang lebih pendek lebih umum, metode ini memungkinkan pengoptimalan kompresi dalam banyak situasi.

Kode dekoder lengkap:

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

Untuk mengkonfigurasi decoder, kita akan melakukan pre-compute kode kanonik, seperti untuk huffman_encoder_init , dan mengisi tabel yang berbeda:

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

Menurunkan


Algoritma Deflate, diperkenalkan pada PKZip 2.04c pada tahun 1993, adalah metode kompresi standar dalam file Zip modern. Ini juga digunakan dalam gzip, PNG dan banyak format lainnya. Ini menggunakan kombinasi kompresi LZ77 dan pengkodean Huffman, yang akan kita bahas dan terapkan di bagian ini.

Sebelum Deflate, PKZip menggunakan metode kompresi Shrink, Reduce, dan Implode. Hari ini mereka jarang, meskipun setelah Deflate masih digunakan untuk beberapa waktu, karena mereka mengkonsumsi lebih sedikit memori. Tapi kami tidak akan mempertimbangkannya.

Bit stream


Deflate menyimpan Huffman codewords dalam bitstream sesuai dengan prinsip LSB-first. Ini berarti bahwa bit pertama dari aliran disimpan dalam bit paling tidak signifikan dari byte pertama.

Pertimbangkan bitstream (baca dari kiri ke kanan) 1-0-0-1-1. Ketika disimpan sesuai dengan prinsip LSB-first, nilai byte menjadi 0b00011001 (biner) atau 0x19 (heksadesimal). Kelihatannya aliran diwakili secara terbalik (dalam arti tertentu), tetapi keuntungannya adalah lebih mudah bagi kita untuk mendapatkan bit N pertama dari kata komputer: kita cukup menyembunyikan bit N paling tidak signifikan.

Prosedur ini diambil dari 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;
}

Decoder Huffman kami perlu melihat bit-bit berikut dalam stream (bit-bit yang cukup untuk kata kode terpanjang), dan kemudian melanjutkan stream dengan jumlah bit yang digunakan oleh simbol yang diterjemahkan:

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

Intinya adalah bahwa pada mesin 64-bit istream_bitsAnda biasanya dapat mengeksekusi sebagai instruksi boot tunggal dan beberapa aritmatika, mengingat bahwa elemen-elemen struktur istream_tada di register. read64le diimplementasikan dalam bits.h (kompiler modern mengubahnya menjadi unduhan 64-bit tunggal menggunakan prinsip 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);
}

Kami juga membutuhkan fungsi untuk melanjutkan bitstream ke perbatasan byte berikutnya:

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

Untuk bitstream keluar, kami menulis bit menggunakan proses baca-modifikasi-tulis. Dalam kasus cepat, Anda dapat menulis sedikit menggunakan 64-bit read, semacam operasi bit dan 64-bit write.

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

Kita juga perlu secara efisien menulis byte ke stream. Tentu saja, Anda dapat berulang kali menjalankan rekaman 8-bit, tetapi akan jauh lebih cepat untuk digunakan 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;
}

Membongkar (Inflasi)


Karena algoritma kompresi disebut Deflate - blowing, mengekstraksi udara dari sesuatu - proses pembongkaran terkadang disebut Inflation . Jika Anda mempelajari proses ini pertama kali, kami akan memahami cara kerja format. Anda dapat melihat kode di bagian pertama dari deflate.h dan deflate.c , bits.h , tables.h dan tables.c (dihasilkan menggunakan generate_tables.c ).

Data yang dikompres menggunakan Deflate disimpan sebagai serangkaian blok.. Setiap blok dimulai dengan header 3-bit, di mana bit pertama (paling tidak signifikan) diatur jika itu adalah blok terakhir dari seri, dan dua bit lainnya menunjukkan jenisnya.


Ada tiga jenis blok: terkompresi (0), dikompresi menggunakan kode Huffman tetap (1), dan dikompresi menggunakan kode Huffman "dinamis" (2).

Kode ini melakukan pembongkaran menggunakan fungsi bantu untuk berbagai jenis blok, yang akan kami implementasikan nanti:

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

Blok Deflate Terkompresi


Ini adalah blok "tersimpan", tipe paling sederhana. Itu dimulai dengan batas 8-bit berikutnya dari bitstream dengan kata 16-bit (len) yang menunjukkan panjang blok. Di belakangnya ada kata 16-bit (nlen) lain, yang melengkapi (urutan bit terbalik) dari kata len. Diasumsikan bahwa nlen bertindak sebagai len checksum sederhana: jika file rusak, maka nilainya mungkin tidak akan komplementer dan program akan dapat mendeteksi kesalahan.


Setelah len dan nlen adalah data yang tidak terkompresi. Karena panjang blok adalah nilai 16-bit, ukuran data dibatasi hingga 65.535 byte.

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

Mengempiskan blok menggunakan kode Huffman tetap


Blok Deflate terkompresi menggunakan kode Huffman untuk mewakili urutan literasi LZ77. Tautan balik rusak menggunakan spidol ujung blok. Untuk literal, panjang backlink, dan marker, kode litlen Huffman digunakan . Dan untuk jarak backlink, kode dist digunakan .


Litlen mengkodekan nilai dalam kisaran 0-285. Nilai 0-255 digunakan untuk byte literal, 256 adalah penanda akhir blok, dan 257-285 digunakan untuk panjang backlink.

Backlinks panjangnya 3-258 byte. Nilai Litlen menentukan panjang dasar, di mana nol atau lebih bit tambahan ditambahkan dari aliran , sehingga panjang penuh diperoleh sesuai dengan tabel di bawah ini. Sebagai contoh, nilai litlen 269 berarti panjang dasar 19 dan dua bit tambahan. Penambahan dua bit dari aliran memberikan panjang akhir dari 19 hingga 22.

LitlenBit ekstraPanjangnya
25703
25804
25905
26006
26107
26208
26309
264010
265111-12
266113-14
267115-16
268117-18
269219-22
270223โ€“26
271227-30
272231โ€“34
273335โ€“42
274343-50
275351โ€“58
276359โ€“66
277467โ€“82
278483โ€“98
279499-114
2804115โ€“130
2815131โ€“162
2825163โ€“194
2835195โ€“226
2845227โ€“257
2850258

Harap dicatat bahwa nilai litlen 284 ditambah 5 bit ekstra dapat mewakili panjang 227 hingga 258, namun spesifikasi menyatakan bahwa panjang 258 - panjang maksimum backlink - harus direpresentasikan menggunakan nilai litlen terpisah. Ini seharusnya mengurangi pengkodean dalam situasi di mana panjang maksimum sering dijumpai.

Dekompresor menggunakan tabel untuk mendapatkan panjang dasar dan bit tambahan dari nilai litlen (minus 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 }
};

Kode litlen tetap Huffman adalah kanonik dan menggunakan panjang codeword berikut (286โ€“287 bukan nilai litlen yang valid, tetapi mereka terlibat dalam pembuatan kode):

Nilai LitlenPanjang Codeword
0โ€“1438
144โ€“2559
256โ€“2797
280โ€“2878

Dekompresor menyimpan panjang-panjang ini dalam tabel yang nyaman untuk transmisi ke huffman_decoder_init:

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

...

/* 287 */ 8,
};

Jarak backlink bervariasi dari 1 hingga 32.768. Mereka dikodekan menggunakan skema yang mirip dengan skema pengkodean panjang. The Huffman code dist mengkodekan nilai dari 0 hingga 29, yang masing-masing sesuai dengan panjang dasar, di mana bit tambahan ditambahkan untuk mendapatkan jarak akhir:

DistBit ekstraJarak
001
102
203
304
415-6
517-8
629-12
7213-16
8317-24
9325โ€“32
10433โ€“48
sebelas449โ€“64
12565โ€“96
tigabelas597โ€“128
146129โ€“192
lima belas6193โ€“256
enambelas7257-384
177385-512
delapan belas8513-768
sembilan belas8769-1024
dua puluh91025-1536
2191537โ€“2048
22102049-3072
23103073โ€“4096
24sebelas4097-6144
25sebelas6145โ€“8192
26128193โ€“12288
271212289โ€“16384
28tigabelas16385โ€“24576
Tanggal 29tigabelas24577โ€“32768

Dist kode tetap Huffman adalah kanonik. Semua codeword panjangnya 5 bit. Sederhana, dekompresor menyimpan kode dalam tabel yang dapat digunakan dengan huffman_decoder_init(nilai dist 30-31 tidak benar. Diindikasikan bahwa mereka terlibat dalam pembuatan kode Huffman, tetapi sebenarnya tidak berpengaruh):

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

...

/* 31 */ 5,
};

Kode Dekompresi atau Pembongkaran - Blok Deflate menggunakan kode Huffman tetap:

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

Perhatikan optimasi ini: ketika tidak ada cukup ruang di buffer keluar, kami mengeluarkan backlink menggunakan fungsi di bawah ini, yang menyalin 64 bit sekaligus. Ini "berantakan" dalam arti sering menyalin beberapa byte tambahan (hingga kelipatan 8 berikutnya). Tetapi ini bekerja jauh lebih cepat lz77_output_backref, karena ia membutuhkan iterasi siklus dan akses memori yang lebih sedikit. Bahkan, backlink pendek sekarang akan diproses dalam satu iterasi, yang sangat baik untuk memprediksi percabangan.

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

Mengempiskan blok menggunakan kode Huffman dinamis


Mengempiskan blok menggunakan kode Huffman dinamis bekerja dengan cara yang sama seperti dijelaskan di atas. Tetapi alih-alih kode yang telah ditentukan untuk litlen dan dist, mereka menggunakan kode yang disimpan dalam aliran Deflate itu sendiri di awal blok. Nama ini mungkin tidak berhasil, karena kode Huffman dinamis juga disebut kode yang berubah selama pengkodean - ini adalah pengkodean Huffman adaptif . Kode yang dijelaskan di sini tidak ada hubungannya dengan prosedur itu. Mereka dinamis hanya dalam arti bahwa blok yang berbeda dapat menggunakan kode yang berbeda.

Membuat kode litlen dan dist dinamis adalah bagian tersulit dari format Deflate. Tetapi segera setelah kode dihasilkan, dekompresi dilakukan dengan cara yang sama seperti yang dijelaskan di bagian sebelumnya, menggunakan 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);
}

Kode litlen dan dist untuk blok Deflate dinamis disimpan sebagai serangkaian panjang kode kata. Panjangnya sendiri dikodekan menggunakan kode Huffman ketiga - codelen . Kode ini ditentukan oleh panjang kata-kata kode ( codelen_lens) yang disimpan dalam blok (apakah saya menyebutkan bahwa itu sulit?).


Pada awal blok dinamis ada 14 bit yang menentukan jumlah panjang kata kode litlen, dist- dan codelen yang perlu dibaca dari blok:

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

Kemudian datang panjang codeword untuk kode codelen. Panjang ini adalah nilai tiga-bit yang biasa, tetapi ditulis dalam urutan khusus yang ditentukan dalam codelen_lengths_order. Karena perlu untuk menentukan 19 panjang, hanya akan dibaca dari sungai num_codelen_lens; semua yang lain secara implisit adalah nol. Panjangnya terdaftar dalam urutan tertentu sehingga panjang nol lebih cenderung jatuh pada akhir daftar dan tidak disimpan dalam blok.

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

Dengan mengatur decoder codelen, kita dapat membaca panjang kata-kata kode litlen dan dist dari stream.

       /* 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 dan 18 bukan panjang sebenarnya, itu adalah indikator bahwa panjang sebelumnya perlu diulang beberapa kali, atau Anda perlu mengulangi panjang nol:

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

Perhatikan bahwa panjang litlen dan dist dibaca satu per satu ke dalam array code_lengths. Mereka tidak dapat dibaca secara terpisah, karena panjang kode berjalan dapat dibawa dari panjang litlen terakhir ke panjang dist pertama.

Setelah menyiapkan panjang kode kata, kita dapat mengonfigurasi decoder Huffman dan kembali ke tugas mendekode literal dan tautan balik:

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

Kompresi (Deflasi)


Di bagian sebelumnya, kami menciptakan semua alat yang diperlukan untuk kompresi Deflate: Lempel-Ziv, pengkodean Huffman, bit stream dan deskripsi dari tiga jenis blok Deflate. Dan di bagian ini kita akan menggabungkan semuanya untuk mendapatkan kompresi Deflate.

Compression Lempel-Ziv mem-parsing data sumber ke dalam urutan backlink dan literal. Urutan ini harus dibagi dan dikodekan ke blok Deflate, seperti yang dijelaskan di bagian sebelumnya. Memilih metode partisi sering disebut pemblokiran.. Di satu sisi, setiap blok baru berarti semacam overhead, volume yang tergantung pada jenis blok dan isinya. Lebih sedikit blok - lebih sedikit overhead. Di sisi lain, biaya pembuatan blok baru ini dapat membuahkan hasil. Misalnya, jika karakteristik data memungkinkan untuk melakukan pengkodean Huffman secara lebih efisien dan mengurangi jumlah total data yang dihasilkan.

Memblokir adalah tugas optimasi yang sulit. Beberapa kompresor (misalnya, Zopfli ) mencoba lebih baik daripada yang lain, tetapi kebanyakan hanya menggunakan pendekatan serakah: mereka mengeluarkan blok segera setelah mereka mencapai ukuran tertentu.

Berbagai jenis blok memiliki batasan ukurannya sendiri:

  • Blok yang tidak terkompresi dapat mengandung tidak lebih dari 65.535 byte.
  • Kode tetap Huffman tidak memiliki ukuran maksimum.
  • Kode Dynamic Huffman umumnya tidak memiliki ukuran maksimum, tetapi karena implementasi algoritma Huffman kami menggunakan urutan karakter 16-bit, kami dibatasi hingga 65.535 karakter.

Untuk menggunakan blok jenis apa pun secara bebas, batasi ukurannya hingga 65.534 byte:

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

Untuk melacak bitstream keluar dan konten blok saat ini selama kompresi, kami akan menggunakan struktur:

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

Untuk menambahkan hasil kerja ke blok, lz77_compresskami akan menggunakan fungsi callback, dan setelah mencapai ukuran maksimum, kami akan menulis blok ke bitstream:

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

Yang paling menarik adalah rekaman balok. Jika blok tidak dikompresi, maka semuanya sederhana:

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

Untuk menulis blok Huffman statis, pertama-tama kita membuat kode kanonik berdasarkan panjang kode sandi tetap untuk kode litlen dan dist. Kemudian kita mengulangi blok, menuliskan karakter yang menggunakan kode ini:

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

Tentu saja, menulis blok dinamis Huffman lebih sulit karena mengandung kode rumit litlen dan kode dist. Untuk mewakili pengkodean ini, kami menggunakan struktur berikut:

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

Pertama, kita membuang ekor nol panjang dari codewords litlen dan dist, dan kemudian menyalinnya ke dalam array reguler untuk pengkodean selanjutnya. Kami tidak dapat membuang semua nol: tidak mungkin untuk menyandikan blok Deflate jika tidak ada kode dist tunggal di dalamnya. Juga tidak mungkin memiliki kurang dari 257 kode litlen, tetapi karena kita selalu memiliki penanda akhir byte, akan selalu ada panjang kode non-nol untuk karakter 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);
}

Setelah menambahkan panjang kode menjadi satu array, kami melakukan pengkodean menggunakan karakter khusus untuk menjalankan panjang kode yang sama.

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

Karakter yang digunakan untuk encoding akan direkam menggunakan kode Huffman - codelen. Panjang kode kata dari kode kode ditulis ke blok dalam urutan tertentu sehingga panjang nol lebih mungkin berakhir di akhir. Berikut adalah fungsi yang menghitung berapa panjang yang harus ditulis:

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

Misalkan kita telah mengatur kode litlen dan dist, mengatur pengkodean panjang kata-kata kode mereka dan kode untuk panjang ini. Sekarang kita dapat menulis blok Huffman dinamis:

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

Untuk setiap blok, kami ingin menggunakan tipe yang membutuhkan jumlah bit paling sedikit. Panjang blok yang tidak dikompresi dapat dihitung dengan cepat:

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

Untuk blok yang disandikan Huffman, Anda dapat menghitung panjang tubuh menggunakan frekuensi karakter litlen dan dist dan panjang codeword:

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

Panjang total blok statis adalah 3 bit header plus panjang tubuh. Menghitung ukuran header dari blok dinamis membutuhkan lebih banyak pekerjaan:

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

Sekarang kita akan menyatukan semuanya dan membuat fungsi utama untuk menulis blok:

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

Akhirnya, inisiator dari seluruh proses kompresi harus mengatur keadaan awal, memulai kompresi Lempel-Ziv dan menulis blok yang dihasilkan:

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

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

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

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

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

        *dst_used = ostream_bytes_written(&s.os);

        return true;
}

Format File Zip


Di atas, kami memeriksa bagaimana kompresi Deflate yang digunakan dalam file Zip bekerja. Bagaimana dengan format file itu sendiri? Pada bagian ini, kita akan memeriksa secara rinci struktur dan implementasinya. Kode ini tersedia dalam zip.h dan zip.c .

Gambaran


Format file dijelaskan dalam Catatan Aplikasi PKZip :

  1. Setiap file, atau item arsip, dalam file zip memiliki header file lokal dengan metadata tentang item.
  2. . , , , Zip-.
  3. , . , . Zip-.


Setiap item arsip dikompresi dan disimpan secara terpisah. Ini berarti bahwa bahkan jika ada kecocokan antara file dalam arsip, mereka tidak akan diperhitungkan untuk meningkatkan kompresi.

Lokasi katalog pusat di bagian akhir memungkinkan Anda untuk secara bertahap melengkapi arsip. Ketika elemen file dikompresi, mereka ditambahkan ke arsip. Indeks direkam setelah semua ukuran terkompresi, yang memungkinkan Anda untuk mengetahui offset semua file. Menambahkan file ke arsip yang ada cukup mudah, ditempatkan setelah elemen terakhir, dan direktori pusat ditimpa.

Kemampuan untuk secara bertahap membuat arsip sangat penting untuk menyebarkan informasi pada banyak disket atau volume. Saat dikompresi, PKZip menyarankan pengguna memasukkan disket baru, dan menulis direktori pusat ke yang terakhir (terakhir). Untuk membuka arsip multi-volume, PKZip pertama-tama meminta disket terakhir untuk menulis untuk membaca direktori pusat, dan kemudian sisa disket yang diperlukan untuk mengekstrak file yang diminta.

Ini mungkin mengejutkan Anda, tetapi tidak ada aturan yang melarang memiliki banyak file dengan nama yang sama di arsip. Ini dapat menyebabkan banyak kebingungan ketika membongkar: jika ada beberapa file dengan nama yang sama, maka yang mana yang harus Anda ekstrak? Pada gilirannya, ini dapat menyebabkan masalah keamanan. Karena bug "Kunci Utama" di Android (CVE-2013-4787 , slide dan video dari laporan tentang Black Hat) penyerang dapat melewati pemeriksaan sistem operasi tanda tangan kriptografi saat menginstal program. Program Android didistribusikan dalam file APK , yang merupakan file Zip. Ternyata, jika APK berisi beberapa file dengan nama yang sama, kode verifikasi tanda tangan memilih file terakhir dengan nama yang sama, dan penginstal memilih file pertama , yaitu, verifikasi tidak dilakukan. Dengan kata lain, perbedaan kecil antara dua pustaka Zip ini memungkinkan untuk mem-bypass seluruh model keamanan sistem operasi.

Tidak seperti kebanyakan format, file zip tidak boleh dimulai dengan tanda tangan atau angka ajaib. Secara umum tidak ditunjukkan bahwa file Zip harus dimulai dengan cara tertentu, yang dengan mudah memungkinkan Anda membuat file yang keduanya valid sebagai Zip dan sebagai format lain - file polyglot . Misalnya, mengekstrak arsip Zip sendiri (misalnya, pkz204g.exe ) biasanya merupakan file yang dapat dieksekusi dan Zip: bagian pertama dapat dieksekusi, diikuti oleh file Zip (yang dibongkar oleh bagian yang dapat dieksekusi). OS dapat menjalankannya sebagai yang dapat dieksekusi, tetapi program Zip akan membukanya sebagai file Zip. Fitur ini dapat menyebabkan Anda tidak memerlukan tanda tangan di awal file.

Meskipun file polyglot tersebut cerdas, mereka dapat menyebabkan masalah keamanan karena mereka dapat menipu program yang mencoba menentukan konten file dan juga memungkinkan pengiriman kode berbahaya di tempat dengan file dari jenis yang berbeda. Misalnya, eksploitasi menggunakan file GIFAR , yang pada saat yang sama adalah gambar GIF yang benar dan arsip Java (JAR, sejenis file Zip). Untuk informasi lebih lanjut tentang masalah ini, lihat artikel Format file yang menyalahgunakan (mulai dari halaman 18).

Seperti yang akan kita lihat di bawah, file Zip menggunakan bidang 32-bit untuk offset dan ukuran untuk membatasi ukuran arsip dan elemen-elemennya hingga empat gigabyte. Dalam Catatan Aplikasi 4.5PKWare telah menambahkan ekstensi format yang memungkinkan penggunaan offset dan ukuran 64-bit. File yang menggunakan ekstensi ini dalam format Zip64, tetapi kami tidak akan mempertimbangkannya.

Struktur data


Akhir dari entri direktori pusat


Akhir entri direktori pusat (EOCDR) biasanya digunakan sebagai titik awal untuk membaca file zip. Ini berisi lokasi dan ukuran direktori pusat, serta komentar opsional tentang seluruh arsip.

Dalam file zip yang ditempati beberapa disket - atau volume - EOCDR juga berisi informasi tentang disk mana yang saat ini kami gunakan, pada disk mana direktori pusat mulai, dll Saat ini, fungsi ini jarang digunakan, dan kode dalam artikel ini tidak memproses file seperti itu.

EOCDR ditentukan oleh tanda tangan 'P' 'K', diikuti oleh byte 5 dan 6. Diikuti oleh struktur di bawah ini, angkanya disimpan sesuai dengan prinsip 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 harus berada di akhir file. Tetapi karena mungkin ada komentar dengan panjang 16-bit sewenang-wenang di ekornya, mungkin perlu untuk menemukan posisi tertentu:

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

Merekam EOCDR itu mudah. Fungsi ini menulis dan mengembalikan jumlah byte yang ditulis:

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

Header file pusat


Direktori pusat terdiri dari header file pusat yang ditulis satu demi satu, satu untuk setiap item arsip. Setiap pos dimulai dengan tanda tangan 'P', 'K', 1, 2, dan kemudian ada struktur seperti itu:

#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_verdan extract_vermenyandikan informasi tentang OS dan versi program yang digunakan untuk menambahkan item ini, serta versi mana yang diperlukan untuk mengambilnya. Delapan bit paling penting menyandikan sistem operasi (misalnya, 0 berarti DOS, 3 berarti Unix, 10 berarti Windows NTFS), dan delapan bit yang lebih rendah menyandikan versi perangkat lunak. Tetapkan nilai desimal ke 20, yang berarti kompatibilitas dengan PKZip 2.0.

gp_flagmengandung berbagai flag. Kami tertarik:

  • Bit 0, menunjukkan fakta enkripsi elemen (kami tidak akan mempertimbangkan ini);
  • Dan bit 1 dan 2, mengkode tingkat kompresi Deflate (0 - normal, 1 - maksimum, 2 - cepat, 3 - sangat cepat).

methodmengkodekan metode kompresi. 0 - data tidak dikompresi, 8 - Delate diterapkan. Nilai-nilai lain berhubungan dengan algoritma lama atau baru, tetapi hampir semua Zip menggunakan kedua nilai ini.

mod_timedan mod_dateberisi tanggal dan waktu file diubah, disandikan dalam format MS-DOS . Dengan menggunakan kode ini, kami akan mengonversi cap waktu C biasa time_tke dan dari format MS-DOS:

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

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

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

        tm.tm_isdst = -1;

        return mktime(&tm);
}

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

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

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

Bidang ini crc32berisi nilai kode redundan siklik dari data yang tidak terkompresi. Ini digunakan untuk memverifikasi integritas data setelah pengambilan. Implementasi di sini: crc32.c .

comp_sizedan uncomp_sizeberisi ukuran terkompresi dan tidak terkompresi dari data file item. Tiga bidang berikut ini berisi panjang nama, komentar, dan data tambahan segera setelah judul. disk_nbr_startDirancang untuk arsip menggunakan banyak disket.

int_attrsdan ext_attrsmenjelaskan atribut internal dan eksternal file. Yang internal berkaitan dengan isi file, misalnya, bit paling signifikan menunjukkan apakah file hanya berisi teks. Atribut eksternal menunjukkan apakah file disembunyikan, hanya-baca, dll. Isi bidang ini tergantung pada OS, khususnya, padamade_by_ver. Dalam DOS, 8 bit yang lebih rendah berisi byte atribut file, yang dapat diperoleh dari panggilan sistem Int 21 / AX = 4300h . Sebagai contoh, bit 4 berarti bahwa itu adalah direktori, dan bit 5 berarti bahwa atribut "arsip" diatur (berlaku untuk sebagian besar file di DOS). Sejauh yang saya mengerti, demi kompatibilitas, bit ini juga diatur di OS lain. Pada Unix, 16 bit tinggi dari bidang ini berisi bit mode file yang dikembalikan oleh stat (2) di st_mode.

lfh_offsetmemberi tahu kami di mana mencari header file lokal. name- nama file (C-line), dan comment- komentar opsional untuk elemen arsip ini (C-line). extradapat berisi data tambahan opsional seperti informasi tentang pemilik file Unix, tanggal dan waktu perubahan yang lebih akurat, atau bidang Zip64.

Fungsi ini digunakan untuk membaca tajuk pusat file:

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

Header file lokal


Data dari setiap elemen arsip didahului oleh tajuk file lokal, yang mengulang sebagian besar informasi dari tajuk pusat.

Duplikasi data di header pusat dan lokal mungkin diperkenalkan sehingga PKZip tidak akan menyimpan seluruh direktori pusat dalam memori saat membongkar. Sebaliknya, karena setiap file diekstraksi, nama dan informasi lainnya dapat dibaca dari header lokal. Selain itu, tajuk lokal berguna untuk memulihkan file dari arsip Zip di mana direktori pusat hilang atau rusak.

Namun, redundansi ini juga merupakan sumber utama ketidakpastian. Misalnya, apa yang terjadi jika nama file di header pusat dan lokal tidak cocok? Ini sering mengarah pada masalah bug dan keamanan.

Tidak semua informasi dari pos pusat digandakan. Misalnya, bidang dengan atribut file. Selain itu, jika bit paling signifikan ketiga gp_flags(CRC-32) diatur, maka bidang terkompresi dan tidak terkompresi akan diatur ulang ke nol, dan informasi ini dapat ditemukan di blok Data Descriptor setelah data file itu sendiri (kami tidak akan mempertimbangkannya). Ini memungkinkan Anda merekam tajuk lokal sebelum ukuran file elemen diketahui atau ukuran apa yang akan dikompres.

Header lokal dimulai dengan tanda tangan 'P', 'K', 3, 4, dan kemudian ada struktur seperti itu:

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

Fungsi-fungsi ini membaca dan menulis header lokal, seperti struktur data lainnya:

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

Penerapan Zip Read


Dengan menggunakan fungsi-fungsi di atas, kami menerapkan pembacaan file Zip ke dalam memori dan mendapatkan iterator untuk mengakses elemen arsip:

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

Seperti yang saya sebutkan di atas, elemen iterator hanyalah offset ke header file pusat di mana Anda dapat mengakses data elemen:

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

Implementasi Catatan Zip


Untuk menulis file zip ke buffer memori, pertama-tama Anda perlu mengetahui berapa banyak memori yang dialokasikan untuk itu. Dan karena kita tidak tahu berapa banyak data yang akan kita kompres sebelum kita mencoba menulis, kita akan menghitung batas atas berdasarkan ukuran elemen yang tidak terkompresi:

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

Kode ini menulis file zip menggunakan kompresi mengempis dari setiap elemen, mengurangi ukurannya:

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


Sekarang kita tahu cara membaca dan menulis file Zip, cara mengompres dan mendekompresi data yang tersimpan di dalamnya. Sekarang mari kita menulis program Zip sederhana yang berisi semua alat ini. Kode ini tersedia di hwzip.c .

Kami akan menggunakan makro untuk penanganan kesalahan sederhana dan beberapa fungsi tambahan untuk alokasi memori yang diperiksa:

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

Dua fungsi lainnya digunakan untuk membaca dan menulis file:

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

Program Zip kami dapat melakukan tiga fungsi: membuat daftar isi file Zip dan mengekstraknya, serta membuat file Zip. Cantuman tidak mudah:

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

Ekstraksi sedikit lebih rumit. Kami akan menggunakan fungsi bantu untuk penghentian nol nama file (untuk meneruskannya fopen) dan membongkar:

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

Program kami akan melewati semua elemen arsip yang memiliki direktori. Ini dilakukan untuk menghindari apa yang disebut serangan jalur traversal : arsip jahat digunakan untuk menulis file dari luar direktori yang ditentukan oleh pengguna. Baca detailnya di FAQ Info-ZIP .

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

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

        zip_data = read_file(filename, &zip_sz);

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

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

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

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

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

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

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

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

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

                free(inflated);
                free(tname);
        }

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

Untuk membuat arsip zip, kita akan membaca file input dan memberi mereka makan zip_write. Karena pustaka C standar tidak memungkinkan Anda untuk mendapatkan waktu modifikasi file, kami akan menggunakan waktu saat ini (saya meninggalkan ini sebagai pekerjaan rumah untuk memperbaiki fitur ini).

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

Akhirnya, ia mainmemeriksa argumen baris perintah dan memutuskan apa yang harus dilakukan:

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

petunjuk perakitan


Satu set lengkap file sumber tersedia di hwzip-1.0.zip . Cara mengompilasi HWZip di Linux atau 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

Di Windows, di prompt perintah pengembang di Visual Studio (jika Anda tidak memiliki Visual Studio, unduh alat pembuatan ):

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 untuk memperluas argumen baris perintah wildcard .)

Kesimpulan


Sungguh menakjubkan bagaimana teknologi berkembang dengan cepat dan lambat. Format Zip dibuat 30 tahun yang lalu berdasarkan teknologi dari tahun lima puluhan dan tujuh puluhan. Dan meskipun banyak yang telah berubah sejak saat itu, file Zip, pada kenyataannya, tetap sama dan hari ini lebih umum daripada sebelumnya. Saya pikir akan bermanfaat untuk memiliki pemahaman yang baik tentang bagaimana mereka bekerja.

Latihan


  • Buat HWZip merekam waktu yang dibutuhkan untuk setiap file berubah, daripada waktu saat ini arsip dibuat. Gunakan stat (2) di Linux atau Mac dan GetFileTime di Windows. Atau tambahkan bendera baris perintah yang memungkinkan pengguna untuk mengatur waktu tertentu untuk perubahan file.
  • gzip-. โ€” , Deflate ( ). RFC 1952.
  • Zip- . HWZip , read_file mmap(2) Linux Mac CreateFileMapping Windows.
  • HWZip , Zip64. appnote.txt.



All Articles