Como é o arquivo zip e o que podemos fazer com ele. Parte 4 - Lendo o arquivo

Continuação do ciclo sobre arquivos Zip e PHP. Artigos anteriores: Parte 1 , Parte 2 , Parte 3

Bom dia, queridos leitores.
Desta vez, gostaria de apresentar, provavelmente, a parte final do ciclo sobre arquivos Zip e PHP.

Neste artigo, mostrarei como ler um arquivo existente e, por exemplo, tiraremos photos.zip de um artigo anterior . Para não repetir todos os procedimentos, usaremos o arquivo pronto - https://github.com/userqq/images/raw/master/photos.zip .

E agora vamos divagar por um momento e lembrar o que nosso arquivo consiste: primeiro, há um conjunto de dados de arquivo compactado, em que cada arquivo compactado é precedido por uma estrutura Local File Header (LFH), depois de todos os dados, temos um conjunto de estruturas de CDFH (Central Directory File Header) - Este é um índice em nosso arquivo, que lista todos os elementos e as posições de seu deslocamento em relação ao início do arquivo. E o arquivo EOCD (Fim do registro de diretório central) é concluído - eis a posição do início das estruturas do CDFH, seu número e comprimento total em bytes. Portanto, o arquivo deve ser lido a partir do final, primeiro para encontrar o EOCD, depois ler as estruturas do CDFH e, assim, obter uma lista de arquivos no arquivo.

FYI: E alguns formatos, como JPEG, são lidos desde o início. Portanto, podemos colar a imagem no arquivo, mesmo que seja brega através do gato image.jpeg archive.zip> imagearchive.jpeg , sem perder a funcionalidade. Navegadores e aplicativos para visualizar fotos nos mostrarão uma imagem sem problemas. Enquanto qualquer aplicativo para leitura de arquivos zip, seja 7z ou descompactado, poderá trabalhar com calma com o arquivo como um arquivo morto. Por exemplo, aqui - https://github.com/userqq/images/blob/master/jpegarchive.jpg (Cuidado, essa coisa pesa cerca de 20 mb, por isso não recomendo abrir o tráfego de telefones ou se você se importa com o tráfego). Portanto, se você conhece a hospedagem de imagens nas quais as imagens não são recodificadas e não são cortadas, você pode enviar não apenas imagens lá :) Embora, ao que me pareça, agora você não as encontre.

Primeiro, definimos uma função auxiliar que facilitará o trabalho de descompactar (eu espiei essa idéia em um dos repositórios para trabalhar com meias e a modifiquei um pouco para o nosso caso):

function readBytes($fh, $formatArray) { //          pack() // https://www.php.net/manual/ru/function.pack.php //        // , L = unsigned long 32bit = 4  static $lengths = ['L' => 4, 'l' => 4, 'i' => 4, 'I' => 4, 'S' => 2, 'a' => 1]; //     , //    fread()     . $totalLength = 0; //      ,     unpack(); $unpackFormat = []; //   ,        -,     0. // ,    ,      0,      . //       $nullData = []; foreach ($formatArray as $name => $format) { $length = 1; //      , , ['versionMadeBy' => 'S'], //       SversionMadeBy //   ['filename' => ['a', $filenameLength]] //    "a{$filenameLength}filename" if (is_array($format)) { [$format, $length] = $format; } //    ,    - //  ['extraFieldLength' => ['a', 0]] //       //      ['extraFieldLength' => null] if ($length < 1) { $nullData[] = $name; continue; } $totalLength += $lengths[$format] * $length; $unpackFormat[] = $format . (($length > 1) ? $length : '') . $name; } $packet = []; if ($totalLength > 0) { $packet = unpack(implode('/', $unpackFormat), fread($fh, $totalLength)); } foreach ($nullData as $empty) { $packet[$empty] = null; } return $packet; } 

Agora vamos tentar encontrar a estrutura EOCD. Seu comprimento mínimo, se não houver comentário, será 22 bytes, dos quais os 4 primeiros são uma assinatura. Portanto, primeiro movemos para a posição tamanho do arquivo ($ arquivo) - 22, lemos os próximos 4 bytes e, se tivermos sorte e esses bytes forem iguais à assinatura (0x06054b50), esse será o nosso EOCD. Se você não tiver sorte - byte por bit, passamos para o início do arquivo até encontrar ou terminar o arquivo - então, provavelmente, este não é um arquivo morto.

 $fh = fopen('photos.zip', 'r'); for ($offset = 22, $length = fstat($fh)['size']; $offset <= $length; $offset++) { fseek($fh, $offset * -1, SEEK_END); // "\x50\x4b\x05\x06" === pack('L', 0x06054b50),  EOCD if ("\x50\x4b\x05\x06" === $bytes = fread($fh, 4)) { echo 'EOCD    ' . ($length - $offset) . PHP_EOL; break; } } $EOCD = readBytes($fh, [ 'diskNumber' => 'S', 'startDiskNumber' => 'S', 'numberCentralDirectoryRecord' => 'S', 'totalCentralDirectoryRecord' => 'S', 'sizeOfCentralDirectory' => 'L', 'centralDirectoryOffset' => 'L', 'commentLength' => 'S', ]); echo '  : ' . $EOCD['numberCentralDirectoryRecord'] . PHP_EOL; echo ' CDFH: ' . $EOCD['centralDirectoryOffset'] . PHP_EOL; 

Agora sabemos quantos elementos temos no arquivo e onde o “índice” começa - uma lista de estruturas do CDFH. Podemos iterar sobre eles e obter os nomes, tamanho dos dados e posição inicial do LFH para cada um dos elementos do arquivo.

 echo '   :' . PHP_EOL; //  ,       CDFH  LFH - $offset = $EOCD['centralDirectoryOffset']; for ($i = 0; $i < $EOCD['numberCentralDirectoryRecord']; $i++) { fseek($fh, $offset, SEEK_SET); // "\x50\x4b\x01\x02" === pack('L', 0x02014b50),  CDFH if ("\x50\x4b\x01\x02" !== $bytes = fread($fh, 4)) { exit('  CDFH' . PHP_EOL); } //  CDFH $CDFH = readBytes($fh, [ 'versionMadeBy' => 'S', 'versionToExtract' => 'S', 'generalPurposeBitFlag' => 'S', 'compressionMethod' => 'S', 'modificationTime' => 'S', 'modificationDate' => 'S', 'crc32' => 'L', 'compressedSize' => 'L', 'uncompressedSize' => 'L', 'filenameLength' => 'S', 'extraFieldLength' => 'S', 'fileCommentLength' => 'S', 'diskNumber' => 'S', 'internalFileAttributes' => 'S', 'externalFileAttributes' => 'L', 'localFileHeaderOffset' => 'L', ]); $CDFH += readBytes($fh, [ 'filename' => ['a', $CDFH['filenameLength']], 'extraField' => ['a', $CDFH['extraFieldLength']], 'fileComment' => ['a', $CDFH['fileCommentLength']], ]); // ,       CDFH $offset = ftell($fh); //   LFH fseek($fh, $CDFH['localFileHeaderOffset'], SEEK_SET); // "\x50\x4b\x03\x04" === pack('L', 0x04034b50),  LFH if ("\x50\x4b\x03\x04" !== $bytes = fread($fh, 4)) { exit('  LFH' . PHP_EOL); } //  LFH $LFH = readBytes($fh, [ 'versionToExtract' => 'S', 'generalPurposeBitFlag' => 'S', 'compressionMethod' => 'S', 'modificationTime' => 'S', 'modificationDate' => 'S', 'crc32' => 'L', 'compressedSize' => 'L', 'uncompressedSize' => 'L', 'filenameLength' => 'S', 'extraFieldLength' => 'S', ]); $LFH += readBytes($fh, [ 'filename' => ['a', $LFH['filenameLength']], 'extraField' => ['a', $LFH['extraFieldLength']], ]); $dataOffset = ftell($fh); echo '> ' . $CDFH['filename'] . ' ' . $CDFH['compressedSize'] . ' , '; echo 'LFH: ' . $CDFH['localFileHeaderOffset'] . ', '; echo ' : ' . $dataOffset . ', '; echo ' : ' . ($dataOffset + $CDFH['compressedSize']); echo PHP_EOL; } fclose($fp); 

Em geral, percorrer o arquivo para frente e para trás para cada um dos registros não é a melhor ideia em termos de desempenho; portanto, recomendo a leitura de todas as estruturas do CDFH e, em seguida, continuo lendo o LFH e os dados, se necessário, mas aqui temos " Programação não convencional ”, para que possamos.

Código de script completo
 <?php function readBytes($fh, $formatArray) { static $lengths = ['L' => 4, 'l' => 4, 'i' => 4, 'I' => 4, 'S' => 2, 'a' => 1]; $totalLength = 0; $unpackFormat = []; $nullData = []; foreach ($formatArray as $name => $format) { $length = 1; if (is_array($format)) { [$format, $length] = $format; } if ($length < 1) { $nullData[] = $name; continue; } $totalLength += $lengths[$format] * $length; $unpackFormat[] = $format . (($length > 1) ? $length : '') . $name; } $packet = []; if ($totalLength > 0) { $packet = unpack(implode('/', $unpackFormat), fread($fh, $totalLength)); } foreach ($nullData as $empty) { $packet[$empty] = null; } return $packet; } $fh = fopen('photos.zip', 'r'); for ($offset = 22, $length = fstat($fh)['size']; $offset <= $length; $offset++) { fseek($fh, $offset * -1, SEEK_END); if ("\x50\x4b\x05\x06" === $bytes = fread($fh, 4)) { echo 'EOCD    ' . ($length - $offset) . PHP_EOL; break; } } $EOCD = readBytes($fh, [ 'diskNumber' => 'S', 'startDiskNumber' => 'S', 'numberCentralDirectoryRecord' => 'S', 'totalCentralDirectoryRecord' => 'S', 'sizeOfCentralDirectory' => 'L', 'centralDirectoryOffset' => 'L', 'commentLength' => 'S', ]); echo '  : ' . $EOCD['numberCentralDirectoryRecord'] . PHP_EOL; echo '  CDFH: ' . $EOCD['centralDirectoryOffset'] . PHP_EOL; echo '   :' . PHP_EOL; $offset = $EOCD['centralDirectoryOffset']; for ($i = 0; $i < $EOCD['numberCentralDirectoryRecord']; $i++) { fseek($fh, $offset, SEEK_SET); if ("\x50\x4b\x01\x02" !== $bytes = fread($fh, 4)) { exit('  CDFH' . PHP_EOL); } $CDFH = readBytes($fh, [ 'versionMadeBy' => 'S', 'versionToExtract' => 'S', 'generalPurposeBitFlag' => 'S', 'compressionMethod' => 'S', 'modificationTime' => 'S', 'modificationDate' => 'S', 'crc32' => 'L', 'compressedSize' => 'L', 'uncompressedSize' => 'L', 'filenameLength' => 'S', 'extraFieldLength' => 'S', 'fileCommentLength' => 'S', 'diskNumber' => 'S', 'internalFileAttributes' => 'S', 'externalFileAttributes' => 'L', 'localFileHeaderOffset' => 'L', ]); $CDFH += readBytes($fh, [ 'filename' => ['a', $CDFH['filenameLength']], 'extraField' => ['a', $CDFH['extraFieldLength']], 'fileComment' => ['a', $CDFH['fileCommentLength']], ]); $offset = ftell($fh); fseek($fh, $CDFH['localFileHeaderOffset'], SEEK_SET); if ("\x50\x4b\x03\x04" !== $bytes = fread($fh, 4)) { exit('  LFH' . PHP_EOL); } $LFH = readBytes($fh, [ 'versionToExtract' => 'S', 'generalPurposeBitFlag' => 'S', 'compressionMethod' => 'S', 'modificationTime' => 'S', 'modificationDate' => 'S', 'crc32' => 'L', 'compressedSize' => 'L', 'uncompressedSize' => 'L', 'filenameLength' => 'S', 'extraFieldLength' => 'S', ]); $LFH += readBytes($fh, [ 'filename' => ['a', $LFH['filenameLength']], 'extraField' => ['a', $LFH['extraFieldLength']], ]); $dataOffset = ftell($fh); echo '> ' . $CDFH['filename'] . ' ' . $CDFH['compressedSize'] . ' , '; echo 'LFH: ' . $CDFH['localFileHeaderOffset'] . ', '; echo ' : ' . $dataOffset . ', '; echo ' : ' . ($dataOffset + $CDFH['compressedSize']); echo PHP_EOL; } fclose($fh); 

Como resultado do script, devemos obter informações sobre o arquivo da seguinte forma:

 $ php readzip.php EOCD    18873702   : 172  CDFH: 18864696    : > 0.jpg 135021 , LFH: 0,  : 35,  : 135056 > 1.jpg 205686 , LFH: 135056,  : 135091,  : 340777 > 2.jpg 81393 , LFH: 340777,  : 340812,  : 422205 > 3.jpg 64892 , LFH: 422205,  : 422240,  : 487132 ... > 171.jpg 50465 , LFH: 18814194,  : 18814231,  : 18864696 

E, em geral, graças a esses dados, já podemos extrair tudo ou um arquivo específico.

Ignoramos a compactação de criptografia e consideramos apenas o caso em que temos os dados no arquivo morto, mas isso não é necessário para entender a estrutura e, para aqueles que querem se confundir, não será difícil ler a especificação e adicionar a funcionalidade ausente com base nesta série de artigos.

Portanto, acho que podemos considerar o ciclo principal concluído. Se você ainda tiver dúvidas ou lembro de algumas menções nos comentários de artigos anteriores, pode haver alguns artigos adicionais.

Para tudo sim.

E espero que, embora tenha sido um pouco para você, ainda seja interessante :)

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


All Articles