À quoi ressemble l'archive zip et ce que nous pouvons en faire. Partie 4 - Lire l'archive

Poursuite du cycle sur les archives Zip et PHP. Articles précédents: Partie 1 , Partie 2 , Partie 3

Bonjour, chers lecteurs.
Cette fois, je voudrais vous présenter, probablement, la dernière partie du cycle sur les archives Zip et PHP.

Dans cet article, je vais montrer comment lire une archive existante et pour un exemple, nous prendrons photos.zip d'un article précédent . Afin de ne pas répéter toutes les procédures, nous utiliserons le prêt à l'emploi - https://github.com/userqq/images/raw/master/photos.zip .

Et maintenant, penchons-nous un instant et rappelons-nous en quoi consiste notre archive: il y a d'abord un ensemble de données de fichier compressé, où chaque fichier compressé est précédé d'une structure d'en-tête de fichier local (LFH), après toutes les données, nous avons un ensemble de structures d'en-tête de fichier de répertoire central (CDFH) - Ceci est une table des matières dans nos archives, qui répertorie tous les éléments et les positions de leur déplacement par rapport au début du fichier. Et l'archive End Of Central Directory Record (EOCD) est terminée - voici la position du début des structures CDFH, leur nombre et leur longueur totale en octets. Par conséquent, l'archive doit être lue à partir de la fin, d'abord pour trouver l'EOCD, puis lire les structures CDFH et ainsi obtenir une liste de fichiers dans l'archive.

FYI: Et certains formats, comme JPEG, sont lus depuis le début. Par conséquent, nous pouvons coller l'image avec l'archive, même ringard via cat image.jpeg archive.zip> imagearchive.jpeg , sans perdre de fonctionnalité. Les navigateurs et les applications de visualisation d'images nous montreront une image sans aucun problème. Bien que toute application de lecture d'archives zip, que ce soit 7z ou décompressez, pourra travailler calmement avec le fichier en tant qu'archive. Par exemple, ici - https://github.com/userqq/images/blob/master/jpegarchive.jpg (Attention, cette chose pèse environ 20 Mo, donc je ne recommande pas d'ouvrir le trafic à partir des téléphones ou si vous vous souciez du trafic). Ainsi, si vous connaissez l'hébergement d'images, sur lesquelles les images ne sont pas recodées et ne sont pas recadrées, vous pouvez y télécharger non seulement des images :) Bien que, il me semble, vous ne pouvez pas les trouver maintenant.

Pour commencer, nous allons définir une fonction auxiliaire qui nous permettra de travailler plus facilement avec le déballage (j'ai espionné cette idée sur un indice dans l'un des référentiels pour travailler avec des chaussettes et l'ai un peu modifiée pour notre cas):

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

Essayons maintenant de trouver la structure EOCD. Sa longueur minimale, s'il n'y a pas de commentaire, sera de 22 octets, dont les 4 premiers sont une signature. Par conséquent, nous passons d'abord à la taille de fichier ($ file) - 22, lisons les 4 octets suivants, et si nous avons de la chance et que ces octets sont égaux à la signature (0x06054b50), alors c'est notre EOCD. Si vous n'êtes pas chanceux - octet par bit, nous passons au début du fichier jusqu'à ce que nous trouvions ou terminions le fichier - alors, ce n'est probablement pas une archive.

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

Nous savons maintenant combien d'éléments nous avons dans l'archive et où commence la «table des matières» - une liste de structures CDFH. Nous pouvons les parcourir et obtenir les noms, la taille des données et la position de départ de LFH pour chacun des éléments d'archive.

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

En général, parcourir le fichier dans les deux sens pour chacun des enregistrements n'est pas la meilleure idée en termes de performances, donc je recommanderais de lire toutes les structures CDFH, puis de lire LFH et les données, si nécessaire, mais nous l'avons ici " Programmation non conventionnelle », afin que nous puissions.

Code de script complet
 <?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); 

À la suite du script, nous devrions obtenir des informations sur l'archive comme suit:

 $ 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 

Et, en général, grâce à ces données, nous pouvons déjà extraire tout ou un fichier spécifique.

Nous avons contourné la compression de chiffrement et considérons uniquement le cas lorsque nous avons les données dans l'archive telles quelles, mais cela n'est pas nécessaire pour comprendre la structure, et pour ceux qui veulent être confus, il ne sera pas difficile de lire la spécification et d'ajouter la fonctionnalité manquante sur la base de cette série d'articles.

Par conséquent, je pense que nous pouvons considérer que le cycle principal est terminé. Si vous avez encore des questions ou si je me souviens de certaines mentions dans les commentaires d'articles précédents, il peut y avoir des articles supplémentaires.

Pour tout sim.

Et j'espère que même si c'était un peu pour toi, c'était quand même intéressant :)

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


All Articles