Seperti apa bentuk arsip zip dan apa yang bisa kita lakukan dengannya. Bagian 2 - Deskriptor dan Kompresi Data

Kelanjutan artikel Bagaimana tampilan arsip zip dan apa yang dapat kita lakukan dengannya .


Kata Pengantar


Hari yang baik
Dan sekali lagi siaran kami memiliki pemrograman tidak konvensional dalam PHP.


Dalam artikel sebelumnya, pembaca yang terhormat tertarik pada kompresi zip dan streaming zip. Hari ini kami akan mencoba sedikit membuka topik ini.


Mari kita lihat

Kode dari artikel terakhir
<?php //        (1.txt  2.txt)   : $entries = [ '1.txt' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc id ante ultrices, fermentum nibh eleifend, ullamcorper nunc. Sed dignissim ut odio et imperdiet. Nunc id felis et ligula viverra blandit a sit amet magna. Vestibulum facilisis venenatis enim sed bibendum. Duis maximus felis in suscipit bibendum. Mauris suscipit turpis eleifend nibh commodo imperdiet. Donec tincidunt porta interdum. Aenean interdum condimentum ligula, vitae ornare lorem auctor in. Suspendisse metus ipsum, porttitor et sapien id, fringilla aliquam nibh. Curabitur sem lacus, ultrices quis felis sed, blandit commodo metus. Duis tincidunt vel mauris at accumsan. Integer et ipsum fermentum leo viverra blandit.', '2.txt' => 'Mauris in purus sit amet ante tempor finibus nec sed justo. Integer ac nibh tempus, mollis sem vel, consequat diam. Pellentesque ut condimentum ex. Praesent finibus volutpat gravida. Vivamus eleifend neque sit amet diam scelerisque lacinia. Nunc imperdiet augue in suscipit lacinia. Curabitur orci diam, iaculis non ligula vitae, porta pellentesque est. Duis dolor erat, placerat a lacus eu, scelerisque egestas massa. Aliquam molestie pulvinar faucibus. Quisque consequat, dolor mattis lacinia pretium, eros eros tempor neque, volutpat consectetur elit elit non diam. In faucibus nulla justo, non dignissim erat maximus consectetur. Sed porttitor turpis nisl, elementum aliquam dui tincidunt nec. Nunc eu enim at nibh molestie porta ut ac erat. Sed tortor sem, mollis eget sodales vel, faucibus in dolor.', ]; //      Lorem.zip,      cwd (      ) $destination = 'Lorem.zip'; $handle = fopen($destination, 'w'); //      ,    ,     ,   "" Central Directory File Header $written = 0; $dictionary = []; foreach ($entries as $filename => $content) { //         Local File Header,     //        ,      . $fileInfo = [ //     'versionToExtract' => 10, //   0,        - 'generalPurposeBitFlag' => 0, //      ,    0 'compressionMethod' => 0, // -    mtime ,    ,      ? 'modificationTime' => 28021, //   , ? 'modificationDate' => 20072, //      .     ,       ,   ? 'crc32' => hexdec(hash('crc32b', $content)), //     .        . //       :) 'compressedSize' => $size = strlen($content), 'uncompressedSize' => $size, //    'filenameLength' => strlen($filename), //  .    ,   0. 'extraFieldLength' => 0, ]; //      . $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, //  Local File Header ] + $fileInfo + ['filename' => $filename])); //       ,       Central Directory File Header $dictionary[$filename] = [ 'signature' => 0x02014b50, //  Central Directory File Header 'versionMadeBy' => 798, //  .    ,  -  . ] + $fileInfo + [ 'fileCommentLength' => 0, //    . No comments 'diskNumber' => 0, //     0,        'internalFileAttributes' => 0, //    'externalFileAttributes' => 2176057344, //    'localFileHeaderOffset' => $written, //      Local File Header 'filename' => $filename, //  . ]; //      $written += fwrite($handle, $LFH); //    $written += fwrite($handle, $content); } // ,     ,    . //          End of central directory record (EOCD) $EOCD = [ //  EOCD 'signature' => 0x06054b50, //  .    ,   0 'diskNumber' => 0, //      -  0 'startDiskNumber' => 0, //       . 'numberCentralDirectoryRecord' => $records = count($dictionary), //    .    ,     'totalCentralDirectoryRecord' => $records, //   Central Directory Record. //      ,      'sizeOfCentralDirectory' => 0, // ,    Central Directory Records 'centralDirectoryOffset' => $written, //     'commentLength' => 0 ]; //     !   foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } // ,   .  ,    $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; //     End of central directory record $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); //  . fclose($handle); echo '  : ' . $written . ' ' . PHP_EOL; echo '     `unzip -tq ' . $destination . '`' . PHP_EOL; echo PHP_EOL; 


Apa masalahnya? Ya, dalam keadilan, perlu dicatat bahwa satu-satunya keuntungannya adalah ia bekerja, dan ada masalah di sana, tapi tetap saja.

Menurut pendapat saya, masalah utama adalah bahwa pertama-tama kita harus menulis Header File Lokal (LFH) dengan crc32 dan panjang file, dan kemudian isi file itu sendiri.
Apa yang mengancam ini? Atau kita memuat seluruh file ke dalam memori, pertimbangkan crc32 untuk itu, tulis LFH , dan kemudian isi file ekonomis dari sudut pandang I / O, tetapi tidak diizinkan dengan file besar. Atau kita membaca file 2 kali - pertama untuk menghitung hash, dan kemudian membaca konten dan menulis ke arsip - secara ekonomis dari sudut pandang RAM, tetapi, misalnya, pertama kali menggandakan beban pada drive, yang belum tentu merupakan SSD.

Dan jika file tersebut terletak jarak jauh dan volumenya, misalnya, 1,5GB? Nah, Anda harus memuat semua 1.5GB ke dalam memori, atau menunggu sampai semua 1.5GB ini diunduh dan kami akan menghitung hash, dan kemudian mengunduhnya lagi untuk memberikan konten. Jika kita ingin memberikan dengan cepat, misalnya, database dump, yang kita, misalnya, baca dari stdout, ini umumnya tidak dapat diterima - data dalam database telah berubah, data dump akan berubah, akan ada hash yang sama sekali berbeda dan kita akan mendapatkan arsip yang tidak valid. Ya, semuanya buruk, tentu saja.


Struktur Deskriptor Data untuk Streaming Arsip Arsip


Tapi jangan berkecil hati, spesifikasi ZIP memungkinkan kita untuk menulis data terlebih dahulu, dan kemudian menempelkan struktur Data Descriptor (DD) setelah data, yang sudah berisi crc32, panjang data yang dikemas dan panjang data tanpa kompresi. Untuk melakukan ini, kita hanya perlu 3 kali sehari dengan perut kosong di LFH tentukan generalPurBitFlag sama dengan 0x0008 , dan crc32 , compressedSize dan uncompressedSize tentukan 0 . Kemudian, setelah data, kita menulis struktur DD , yang akan terlihat seperti ini:



 pack('LLLL', ...array_values([ 'signature' => 0x08074b50, //  Data Descriptor 'crc32' => $crc32, //  crc32    'compressedSize' => $compressedSize, //    'uncompressedSize' => $uncompressedSize, //    . ])); 

Dan di Header Direktori File Pusat (CDFH) hanya perubahan GeneralPurposeBitFlag , sisa data harus nyata. Tapi ini bukan masalah, karena kita menulis CDFH setelah semua data, dan hash dengan panjang data diketahui dalam hal apa pun.


Ini semua tentu saja bagus. Tetap hanya menerapkan di PHP.
Dan perpustakaan Hash standar akan banyak membantu kami. Kita dapat membuat konteks hash di mana itu akan cukup untuk mengisi potongan data, dan pada akhirnya mendapatkan nilai hash. Tentu saja, solusi ini akan sedikit lebih rumit daripada hash ('crc32b', $ content) , tetapi ini akan menghemat banyak sumber daya dan waktu yang tidak terbayangkan.

Itu terlihat seperti ini:

 $hashCtx = hash_init('crc32b'); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $chunk = null; } $hash = hash_final($hashCtx); 

Jika semuanya dilakukan dengan benar, maka nilainya tidak akan berbeda sama sekali dari hash_file ('crc32b', $ source) atau hash ('crc32b', file_get_content ($ source)) .

Mari kita coba untuk membungkus semuanya dalam satu fungsi, sehingga kita dapat membaca file dengan cara yang mudah bagi kita, dan pada akhirnya mendapatkan hash dan panjangnya. Dan generator akan membantu kita dengan ini:

 function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; } 

dan sekarang kita bisa adil

 $reader = read('https://speed.hetzner.de/1GB.bin'); foreach ($reader as $chunk) { // -   . } //      . ['length' => $length, 'crc32' => $crc32] = $reader->getReturn(); echo round(memory_get_peak_usage(true) / 1024 / 1024, 2) . 'MB - Memory Peak Usage' . PHP_EOL; 

Menurut saya itu cukup sederhana dan nyaman. Dengan file 1GB, konsumsi memori puncak saya adalah 2MB.

Sekarang mari kita coba memodifikasi kode dari artikel sebelumnya sehingga kita dapat menggunakan fungsi ini.

Naskah akhir
 <?php function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10, //       Data Descriptor,     00008, //   00000    . 'generalPurposeBitFlag' => 0x0008, 'compressionMethod' => 0, 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => 0, 'compressedSize' => 0, 'uncompressedSize' => 0, 'filenameLength' => strlen($filename), 'extraFieldLength' => 0, ]; $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, ] + $fileInfo + ['filename' => $filename])); $fileOffset = $written; $written += fwrite($handle, $LFH); //     $reader = read($entry); foreach ($reader as $chunk) { //      $written += fwrite($handle, $chunk); $chunk = null; } //       ['length' => $length, 'crc32' => $crc32] = $reader->getReturn(); //    fileInfo,     CDFH $fileInfo['crc32'] = $crc32; $fileInfo['compressedSize'] = $length; $fileInfo['uncompressedSize'] = $length; //  Data Descriptor $DD = pack('LLLL', ...array_values([ 'signature' => 0x08074b50, 'crc32' => $fileInfo['crc32'], 'compressedSize' => $fileInfo['compressedSize'], 'uncompressedSize' => $fileInfo['uncompressedSize'], ])); $written += fwrite($handle, $DD); $dictionary[$filename] = [ 'signature' => 0x02014b50, 'versionMadeBy' => 798, ] + $fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $fileOffset, 'filename' => $filename, ]; } $EOCD = [ 'signature' => 0x06054b50, 'diskNumber' => 0, 'startDiskNumber' => 0, 'numberCentralDirectoryRecord' => $records = count($dictionary), 'totalCentralDirectoryRecord' => $records, 'sizeOfCentralDirectory' => 0, 'centralDirectoryOffset' => $written, 'commentLength' => 0 ]; foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); fclose($handle); echo '  : ' . memory_get_peak_usage(true) . ' ' . PHP_EOL; echo '  : ' . $written . ' ' . PHP_EOL; echo '   `unzip -tq ' . $destination . '`: ' . PHP_EOL; echo '> ' . exec('unzip -tq ' . $destination) . PHP_EOL; echo PHP_EOL; 


Pada output, kita harus mendapatkan arsip Zip dengan nama test.zip, di mana akan ada file dengan skrip di atas dan 100MB.bin, berukuran sekitar 100 MB.

Kompresi dalam arsip zip


Sekarang kita memiliki hampir segalanya untuk mengompresi data dan melakukannya juga dengan cepat.
Sama seperti kita mendapatkan hash dengan memberikan potongan kecil untuk fungsi, kita juga dapat memampatkan data berkat pustaka Zlib yang luar biasa dan fungsi deflate_init dan deflate_add .


Itu terlihat seperti ini:

 $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); yield deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $chunk = null; } 

Saya bertemu opsi seperti ini bahwa, dibandingkan dengan yang sebelumnya, itu akan menambahkan beberapa nol di akhir.
Judul spoiler
 while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH); 

Tapi unzip bersumpah, jadi aku harus menyingkirkan penyederhanaan seperti itu.

Mari kita perbaiki pembaca kita sehingga segera memampatkan data kita, dan pada akhirnya mengembalikan kita hash, panjang data tanpa kompresi dan panjang data dengan kompresi:

 function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; } 

dan coba pada file 100 mb:

 $reader = read('https://speed.hetzner.de/100MB.bin'); foreach ($reader as $chunk) { // -   . } ['uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => $crc32] = $reader->getReturn(); echo 'Uncompressed size: ' . $uncompressedSize . PHP_EOL; echo 'Compressed size: ' . $compressedSize . PHP_EOL; echo round(memory_get_peak_usage(true) / 1024 / 1024, 2) . 'MB - Memory Peak Usage' . PHP_EOL; 

Konsumsi memori masih menunjukkan bahwa kami tidak memuat seluruh file ke dalam memori.

Mari kita kumpulkan semuanya dan akhirnya dapatkan pengarsipan script yang benar-benar nyata.
Berbeda dengan versi sebelumnya, generalPurposeBitFlag kami akan berubah - sekarang nilainya 0x0018 , serta compressionMethod - 8 (yang berarti Deflate ).

Naskah akhir
 <?php function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10, //   ,        0x0018  0x0008 'generalPurposeBitFlag' => 0x0018, 'compressionMethod' => 8, //      : 8 - Deflate 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => 0, 'compressedSize' => 0, 'uncompressedSize' => 0, 'filenameLength' => strlen($filename), 'extraFieldLength' => 0, ]; $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, ] + $fileInfo + ['filename' => $filename])); $fileOffset = $written; $written += fwrite($handle, $LFH); $reader = read($entry); foreach ($reader as $chunk) { $written += fwrite($handle, $chunk); $chunk = null; } [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => $crc32 ] = $reader->getReturn(); $fileInfo['crc32'] = $crc32; $fileInfo['compressedSize'] = $compressedSize; $fileInfo['uncompressedSize'] = $uncompressedSize; $DD = pack('LLLL', ...array_values([ 'signature' => 0x08074b50, 'crc32' => $fileInfo['crc32'], 'compressedSize' => $fileInfo['compressedSize'], 'uncompressedSize' => $fileInfo['uncompressedSize'], ])); $written += fwrite($handle, $DD); $dictionary[$filename] = [ 'signature' => 0x02014b50, 'versionMadeBy' => 798, ] + $fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $fileOffset, 'filename' => $filename, ]; } $EOCD = [ 'signature' => 0x06054b50, 'diskNumber' => 0, 'startDiskNumber' => 0, 'numberCentralDirectoryRecord' => $records = count($dictionary), 'totalCentralDirectoryRecord' => $records, 'sizeOfCentralDirectory' => 0, 'centralDirectoryOffset' => $written, 'commentLength' => 0 ]; foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); fclose($handle); echo '  : ' . memory_get_peak_usage(true) . ' ' . PHP_EOL; echo '  : ' . $written . ' ' . PHP_EOL; echo '   `unzip -tq ' . $destination . '`: ' . PHP_EOL; echo '> ' . exec('unzip -tq ' . $destination) . PHP_EOL; echo PHP_EOL; 

Akibatnya, saya mendapatkan arsip berukuran 360183 byte (file 100MB kami dikompresi dengan sangat baik, di mana kemungkinan besar hanya satu set byte yang identik), dan unzip menunjukkan bahwa tidak ada kesalahan yang ditemukan dalam arsip.

Kesimpulan


Jika saya memiliki energi dan waktu yang cukup untuk artikel lain, maka saya akan mencoba menunjukkan bagaimana dan, yang paling penting, mengapa semua ini dapat digunakan.

Jika Anda tertarik pada hal lain tentang topik ini - sarankan di komentar, saya akan mencoba memberikan jawaban untuk pertanyaan Anda. Kemungkinan besar kita tidak akan berurusan dengan enkripsi, karena skrip sudah tumbuh, dan dalam kehidupan nyata arsip seperti itu, menurut saya, jarang digunakan.



Terima kasih atas perhatian dan komentar Anda.

Source: https://habr.com/ru/post/id472966/


All Articles