À quoi ressemble l'archive zip et ce que nous pouvons en faire. Partie 2 - Descripteur de donnĂ©es et compression

Suite de l'article A quoi ressemble l'archive zip et ce que nous pouvons en faire .


Préface


Bonjour.
Et encore une fois Ă  l'antenne, nous avons une programmation non conventionnelle en PHP.


Dans un article précédent, les lecteurs estimés s'intéressaient à la compression zip et au streaming zip. Aujourd'hui, nous allons essayer d'ouvrir un peu ce sujet.


Jetons un coup d'oeil Ă 

Code du dernier article
<?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; 


Quel est son problĂšme? Eh bien, en toute justice, il convient de noter que son seul avantage est que cela fonctionne, et il y a des problĂšmes lĂ -bas, mais quand mĂȘme.

À mon avis, le principal problĂšme est que nous devons d'abord Ă©crire l'en- tĂȘte de fichier local (LFH) avec crc32 et la longueur du fichier, puis le contenu du fichier lui-mĂȘme.
Qu'est-ce que cela menace? Ou nous chargeons l'intégralité du fichier en mémoire, considérons crc32 pour cela, écrivons LFH , puis le contenu du fichier est économique du point de vue des E / S, mais n'est pas autorisé avec les gros fichiers. Ou nous lisons le fichier 2 fois - d'abord pour calculer le hachage, puis pour lire le contenu et écrire dans l'archive - économiquement du point de vue de la RAM, mais, par exemple, tout d'abord, il double la charge sur le lecteur, qui n'est pas nécessairement un SSD.

Et si le fichier est situé à distance et son volume, par exemple, 1,5 Go? Eh bien, vous devez soit charger tous les 1,5 Go en mémoire, soit attendre que tous ces 1,5 Go soient téléchargés et nous calculerons le hachage, puis les télécharger à nouveau pour donner le contenu. Si nous voulons donner à la volée, par exemple, une base de données de vidage, que nous lisons, par exemple, depuis stdout, cela est généralement inacceptable - les données de la base de données ont changé, les données de vidage changeront, il y aura un hachage complÚtement différent et nous obtiendrons une archive invalide. Oui, les choses vont mal, bien sûr.


Structure de descripteur de données pour les enregistrements d'archives en streaming


Mais ne vous découragez pas, la spécification ZIP nous permet d'écrire d'abord des données, puis de coller la structure Data Descriptor (DD) aprÚs les données, qui contient déjà crc32, la longueur des données compressées et la longueur des données sans compression. Pour ce faire, nous n'avons besoin que de 3 fois par jour à jeun dans LFH, spécifiez generalPurposeBitFlag égal à 0x0008 , et crc32 , compressSize et uncompressedSize spécifiez 0 . Ensuite, aprÚs les données, nous écrivons la structure DD , qui ressemblera à ceci:



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

Et dans l' en-tĂȘte de fichier de rĂ©pertoire central (CDFH), seuls les changements de GeneralPurposeBitFlag , le reste des donnĂ©es doit ĂȘtre rĂ©el. Mais ce n'est pas un problĂšme, car nous Ă©crivons CDFH aprĂšs toutes les donnĂ©es, et les hachages avec des longueurs de donnĂ©es sont connus dans tous les cas.


C'est bien sûr bien. Il ne reste plus qu'à l'implémenter en PHP.
Et la bibliothÚque Hash standard nous aidera beaucoup. Nous pouvons créer un contexte de hachage dans lequel il suffira de bourrer des morceaux de données et, finalement, d'obtenir la valeur de hachage. Bien sûr, cette solution sera un peu plus encombrante que le hachage («crc32b», $ content) , mais elle nous fera économiser un tas inimaginable de ressources et de temps.

Cela ressemble Ă  ceci:

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

Si tout est fait correctement, la valeur ne différera pas du tout de hash_file ('crc32b', $ source) ou hash ('crc32b', file_get_content ($ source)) .

Essayons de résumer tout cela dans une seule fonction, afin que nous puissions lire le fichier d'une maniÚre pratique pour nous, et finalement obtenir son hachage et sa longueur. Et les générateurs nous aideront avec ceci:

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

et maintenant on peut juste

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

À mon avis, c'est assez simple et pratique. Avec un fichier de 1 Go, ma consommation de mĂ©moire maximale Ă©tait de 2 Mo.

Essayons maintenant de modifier le code de l'article précédent pour pouvoir utiliser cette fonction.

Script final
 <?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; 


À la sortie, nous devrions obtenir une archive Zip avec le nom test.zip, dans laquelle il y aura un fichier avec le script ci-dessus et 100 Mo.bin, d'environ 100 Mo de taille.

Compression dans les archives zip


Maintenant, nous avons pratiquement tout pour compresser les données et le faire aussi à la volée.
Tout comme nous obtenons un hachage en donnant de petits morceaux aux fonctions, nous pouvons également compresser les données grùce à la merveilleuse bibliothÚque Zlib et ses fonctions deflate_init et deflate_add .


Cela ressemble Ă  ceci:

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

J'ai rencontré une option comme celle-ci qui, par rapport à la précédente, ajoutera quelques zéros à la fin.
Cap de spoiler
 while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH); 

Mais décompressez juré, j'ai donc dû me débarrasser d'une telle simplification.

Corrigeons notre lecteur pour qu'il comprime immédiatement nos données, et à la fin nous retourne un hachage, la longueur des données sans compression et la longueur des données avec compression:

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

et essayez un fichier de 100 Mo:

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

La consommation de mémoire montre toujours que nous n'avons pas chargé le fichier entier en mémoire.

Mettons tout cela ensemble et obtenons enfin un véritable archiveur de scripts.
Contrairement à la version précédente, notre generalPurposeBitFlag va changer - maintenant sa valeur est 0x0018 , ainsi que compressionMethod - 8 (ce qui signifie Deflate ).

Script final
 <?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; 

En conséquence, j'ai obtenu une archive de 360183 octets (notre fichier de 100 Mo a été trÚs bien compressé, dans lequel trÚs probablement un ensemble d'octets identiques), et décompresser a montré qu'aucune erreur n'a été trouvée dans l'archive.

Conclusion


Si j'ai suffisamment d'Ă©nergie et de temps pour un autre article, je vais essayer de montrer comment et, surtout, pourquoi tout cela peut ĂȘtre utilisĂ©.

Si vous ĂȘtes intĂ©ressĂ© par autre chose sur ce sujet - suggĂ©rez dans les commentaires, je vais essayer de rĂ©pondre Ă  votre question. TrĂšs probablement, nous ne traiterons pas du cryptage, car le script a dĂ©jĂ  grandi et, dans la vraie vie, de telles archives, il me semble, ne sont pas utilisĂ©es trĂšs souvent.



Merci de votre attention et de vos commentaires.

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


All Articles