Como é o arquivo zip e o que podemos fazer com ele. Parte 2 - Descritor e compactação de dados

Continuação do artigo Como é o arquivo zip e o que podemos fazer com ele .


Prefácio


Bom dia
E novamente no ar, temos programação não convencional em PHP.


Em um artigo anterior, estimados leitores estavam interessados ​​em compactação e streaming de zip. Hoje vamos tentar abrir um pouco esse tópico.


Vamos dar uma olhada

Código do último artigo
<?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; 


Qual é o problema dele? Bem, para ser justo, vale a pena notar que sua única vantagem é que ele funciona, e há problemas lá, mas ainda assim.

Na minha opinião, o principal problema é que devemos primeiro escrever o Local File Header (LFH) com crc32 e o tamanho do arquivo e, em seguida, o conteúdo do próprio arquivo.
O que isso ameaça? Ou carregamos o arquivo inteiro na memória, consideramos o crc32 para ele, escrevemos LFH e, em seguida, o conteúdo do arquivo é econômico do ponto de vista da E / S, mas é inaceitável em arquivos grandes. Ou então, lemos o arquivo duas vezes - primeiro para calcular o hash, e depois para ler o conteúdo e gravar no arquivo - economicamente do ponto de vista da RAM, mas, por exemplo, primeiro dobra a carga na unidade, o que não é necessariamente um SSD.

E se o arquivo estiver localizado remotamente e seu volume, por exemplo, 1,5 GB? Bem, você precisa carregar todos os 1,5 GB na memória ou esperar até que todos esses 1,5 GB sejam baixados e calcularemos o hash e depois baixá-los novamente para fornecer o conteúdo. No caso, se quisermos fornecer dinamicamente, por exemplo, um banco de dados de despejo, que, por exemplo, lemos no stdout, isso geralmente é inaceitável - os dados no banco de dados foram alterados, os dados de despejo serão alterados, haverá um hash completamente diferente e obteremos um arquivo inválido. Sim, as coisas estão ruins, é claro.


Estrutura do descritor de dados para registros de arquivamento de fluxo contínuo


Mas não desanime, a especificação ZIP permite escrever dados primeiro e depois colar a estrutura do Descritor de Dados (DD) após os dados, nos quais já haverá crc32, o comprimento dos dados compactados e o comprimento dos dados sem compactação. Para fazer isso, precisamos apenas 3 vezes por dia com o estômago vazio no LFH, especificar generalPurposeBitFlag igual a 0x0008 e crc32 , compressedSize e uncompressedSize especificar 0 . Depois dos dados, escrevemos a estrutura DD , que será algo como isto:



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

E no CDFH (Central Directory File Header) apenas o generalPurposeBitFlag é alterado , o restante dos dados deve ser real. Mas isso não é um problema, já que escrevemos CDFH após todos os dados, e hashes com comprimentos de dados são conhecidos em qualquer caso.


Isso é tudo, é claro, bom. Resta apenas implementar no PHP.
E a biblioteca Hash padrão nos ajudará bastante. Podemos criar um contexto de hash no qual será suficiente para encher pedaços com dados e, no final, obter o valor do hash. Obviamente, essa solução será um pouco mais complicada que o hash ('crc32b', $ content) , mas nos poupará apenas um monte inimaginável de recursos e tempo.

Parece algo como isto:

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

Se tudo for feito corretamente, o valor não será diferente de hash_file ('crc32b', $ source) ou hash ('crc32b', file_get_content ($ source)) .

Vamos tentar, de alguma forma, agrupar tudo isso em uma única função, para que possamos ler o arquivo de uma maneira conveniente para nós e, no final, obter seu hash e comprimento. E os geradores nos ajudarão com isso:

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

e agora podemos apenas

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

Na minha opinião, é bastante simples e conveniente. Com um arquivo de 1 GB, meu consumo máximo de memória era de 2 MB.

Agora vamos tentar modificar o código do artigo anterior para que possamos usar esta função.

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


Na saída, devemos obter um arquivo Zip com o nome test.zip, no qual haverá um arquivo com o script acima e 100MB.bin, com aproximadamente 100 MB de tamanho.

Compactação em arquivos zip


Agora, temos praticamente tudo para compactar os dados e fazê-lo também em tempo real.
Assim como obtemos um hash atribuindo pequenos blocos às funções, também podemos compactar dados graças à maravilhosa biblioteca Zlib e suas funções deflate_init e deflate_add .


Parece algo como isto:

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

Eu encontrei uma opção como esta que, em comparação com a anterior, adicionará alguns zeros no final.
Título de spoiler
 while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH); 

Mas descompacte o juramento, então tive que me livrar dessa simplificação.

Vamos corrigir nosso leitor para compactar imediatamente nossos dados e, no final, retornar um hash, o comprimento dos dados sem compactação e o comprimento dos dados com compactação:

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

e experimente um arquivo de 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; 

O consumo de memória ainda mostra que não carregamos o arquivo inteiro na memória.

Vamos juntar tudo e finalmente obter um arquivador de scripts realmente real.
Diferentemente da versão anterior, nosso generalPurposeBitFlag mudará - agora seu valor é 0x0018 , assim como o compressionMethod - 8 (que significa Deflate ).

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

Como resultado, obtive um arquivo de 360183 bytes de tamanho (nosso arquivo de 100 MB foi compactado muito bem, no qual provavelmente apenas um conjunto de bytes idênticos), e o descompactação mostrou que nenhum erro foi encontrado no arquivo.

Conclusão


Se eu tiver energia e tempo suficientes para outro artigo, tentarei mostrar como e, o mais importante, por que tudo isso pode ser usado.

Se você estiver interessado em mais alguma coisa sobre esse tópico - sugira nos comentários, tentarei responder à sua pergunta. Provavelmente não lidaremos com criptografia, porque o script já cresceu e, na vida real, esses arquivos, ao que me parece, não são usados ​​com muita frequência.



Obrigado por sua atenção e por seus comentários.

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


All Articles