Wie das Zip-Archiv aussieht und was wir damit machen können. Teil 4 - Das Archiv lesen

Fortsetzung des Zyklus über Zip-Archive und PHP. Bisherige Artikel: Teil 1 , Teil 2 , Teil 3

Guten Tag, liebe Leser.
Dieses Mal möchte ich wahrscheinlich den letzten Teil des Zyklus über Zip-Archive und PHP vorstellen.

In diesem Artikel werde ich zeigen, wie man ein vorhandenes Archiv liest, und als Beispiel nehmen wir photos.zip aus einem vorherigen Artikel . Um nicht alle Vorgänge zu wiederholen, verwenden wir das vorgefertigte https://github.com/userqq/images/raw/master/photos.zip .

Lassen Sie uns nun einen Moment abschweifen und uns daran erinnern, woraus unser Archiv besteht: Zuerst gibt es einen Satz gepackter Dateidaten, wobei jeder gepackten Datei eine lokale Dateikopfstruktur (Local File Header, LFH) vorangestellt ist, nachdem alle Daten einen Satz zentraler Verzeichnisdateikopfstrukturen (Central Directory File Header, CDFH) enthalten - Dies ist ein Inhaltsverzeichnis in unserem Archiv, das alle Elemente und die Positionen ihrer Verschiebung relativ zum Dateianfang auflistet. Und das EOCD-Archiv (End Of Central Directory Record) ist fertig - hier ist die Position des Anfangs der CDFH-Strukturen, ihre Anzahl und Gesamtlänge in Byte. Daher sollte das Archiv von Anfang an gelesen werden, um zuerst das EOCD zu finden, dann die CDFH-Strukturen zu lesen und somit eine Liste der Dateien im Archiv zu erhalten.

Zu Ihrer Information: Einige Formate, wie JPEG, werden von Anfang an gelesen. Daher können wir das Bild mit dem Archiv zusammenkleben, sogar kitschig durch cat image.jpeg archive.zip> imagearchive.jpeg , ohne die Funktionalität zu verlieren. Browser und Anwendungen zum Anzeigen von Bildern zeigen uns problemlos ein Bild. Während jede Anwendung zum Lesen von Zip-Archiven, sei es 7z oder Entpacken, in der Lage ist, ruhig mit der Datei als Archiv zu arbeiten. Zum Beispiel hier - https://github.com/userqq/images/blob/master/jpegarchive.jpg (Achtung, dieses Ding wiegt ungefähr 20 MB, daher empfehle ich nicht, den Datenverkehr über Telefone zu öffnen oder wenn Sie sich für den Datenverkehr interessieren). Wenn Sie also das Hosting von Bildern kennen, auf denen die Bilder nicht neu codiert und nicht zugeschnitten sind, können Sie nicht nur Bilder dort hochladen.

Zunächst definieren wir eine Hilfsfunktion, die uns die Arbeit mit dem Auspacken erleichtert (ich habe diese Idee in einem der Repositorys für die Arbeit mit Socken ausspioniert und ein wenig für unseren Fall modifiziert):

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

Versuchen wir nun, die EOCD-Struktur zu finden. Wenn kein Kommentar vorhanden ist, beträgt die Mindestlänge 22 Byte, von denen die ersten 4 eine Signatur sind. Deshalb bewegen wir uns zuerst zur Position Dateigröße ($ file) - 22, lesen die nächsten 4 Bytes und wenn wir Glück haben und diese Bytes gleich der Signatur sind (0x06054b50), dann ist dies unser EOCD. Wenn Sie kein Glück haben - byteweise bewegen wir uns zum Anfang der Datei, bis wir die Datei gefunden oder beendet haben - dann ist dies wahrscheinlich kein Archiv.

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

Jetzt wissen wir, wie viele Elemente wir im Archiv haben und wo das „Inhaltsverzeichnis“ beginnt - eine Liste von CDFH-Strukturen. Wir können sie durchlaufen und die Namen, die Datengröße und die LFH-Startposition für jedes der Archivelemente ermitteln.

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

Im Allgemeinen ist es in Bezug auf die Leistung nicht die beste Idee, die Datei für jeden Datensatz hin und her zu durchsuchen. Daher würde ich empfehlen, alle CDFH-Strukturen zu lesen und dann, falls erforderlich, LFH und Daten zu lesen. Unkonventionelle Programmierung “, so können wir.

Vollständiger Skriptcode
 <?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); 

Als Ergebnis des Skripts sollten wir folgende Informationen über das Archiv erhalten:

 $ 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 

Und im Allgemeinen können wir dank dieser Daten bereits alles oder eine bestimmte Datei extrahieren.

Wir haben die Verschlüsselungskomprimierung umgangen und betrachten nur den Fall, in dem wir die Daten im Archiv haben, aber dies ist nicht erforderlich, um die Struktur zu verstehen, und für diejenigen, die sich verwirren möchten, wird es nicht schwierig sein, die Spezifikation zu lesen und die fehlende Funktionalität basierend auf dieser Artikelserie hinzuzufügen.

Daher denke ich, dass wir den Hauptzyklus als beendet betrachten können. Wenn Sie noch Fragen haben oder ich mich an einige Erwähnungen in den Kommentaren zu früheren Artikeln erinnern werde, gibt es möglicherweise einige zusätzliche Artikel.

Für sim alles.

Und ich hoffe, dass es, obwohl es ein bisschen für dich war, immer noch interessant war :)

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


All Articles