Seperti apa bentuk arsip zip dan apa yang bisa kita lakukan dengannya. Bagian 4 - Membaca arsip

Kelanjutan dari siklus tentang arsip Zip dan PHP. Artikel sebelumnya: Bagian 1 , Bagian 2 , Bagian 3

Selamat siang, pembaca yang budiman.
Kali ini saya ingin memperkenalkan, mungkin, bagian akhir dari siklus tentang arsip Zip dan PHP.

Pada artikel ini saya akan menunjukkan cara membaca arsip yang ada dan sebagai contoh kita akan mengambil foto.zip dari artikel sebelumnya . Agar tidak mengulangi semua prosedur, kami akan menggunakan yang sudah jadi - https://github.com/userqq/images/raw/master/photos.zip .

Dan sekarang mari kita menyimpang sejenak dan ingat apa yang terdiri dari arsip kita: pertama ada satu set data file yang dikemas, di mana setiap file yang dikemas diawali oleh struktur Header File Lokal (LFH), setelah semua data kita memiliki satu set struktur Header File Direktori Utama (CDFH) - Ini adalah daftar isi dalam arsip kami, yang mencantumkan semua elemen dan posisi perpindahannya relatif ke awal file. Dan arsip Catatan Direktori Tengah (EOCD) selesai - di sini adalah posisi awal dari struktur CDFH, jumlah dan panjang total dalam byte. Oleh karena itu, arsip harus dibaca dari akhir, pertama untuk menemukan EOCD, kemudian membaca struktur CDFH dan dengan demikian mendapatkan daftar file dalam arsip.

FYI: Dan beberapa format, seperti JPEG, dibaca dari awal. Oleh karena itu, kita dapat merekatkan gambar dengan arsip, bahkan norak melalui cat image.jpeg archive.zip> imagearchive.jpeg , tanpa kehilangan fungsionalitas. Browser dan aplikasi untuk melihat gambar akan menunjukkan kepada kita gambar tanpa masalah. Sementara aplikasi apa pun untuk membaca arsip zip, baik itu 7z atau unzip, akan dapat bekerja dengan tenang dengan file sebagai arsip. Misalnya, di sini - https://github.com/userqq/images/blob/master/jpegarchive.jpg (Perhatian, benda ini beratnya sekitar 20mb, jadi saya tidak menyarankan membuka lalu lintas dari ponsel atau jika Anda peduli tentang lalu lintas). Jadi, jika Anda tahu hosting gambar yang gambarnya tidak dikode ulang dan tidak dipotong, Anda dapat mengunggah tidak hanya gambar di sana :) Meskipun, menurut saya, sekarang Anda tidak dapat menemukannya.

Pertama, kita mendefinisikan fungsi pembantu yang akan memudahkan kita untuk bekerja dengan membongkar (Saya melihat ide ini pada petunjuk di salah satu repositori untuk bekerja dengan kaus kaki dan memodifikasinya sedikit untuk kasus kita):

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

Sekarang mari kita cari struktur EOCD. Panjang minimumnya, jika tidak ada komentar, akan menjadi 22 byte, dimana 4 pertama adalah tanda tangan. Oleh karena itu, pertama-tama kita pindah ke posisi fileize ($ file) - 22, baca 4 byte berikutnya, dan jika kita beruntung dan byte ini sama dengan tanda tangan (0x06054b50), maka ini adalah EOCD kita. Jika Anda tidak beruntung - byte-demi-bit kami pindah ke awal file sampai kami menemukan atau menyelesaikan file - maka, mungkin, ini bukan arsip.

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

Sekarang kita tahu berapa banyak elemen yang kita miliki di arsip dan di mana "daftar isi" dimulai - daftar struktur CDFH. Kita dapat mengulanginya dan mendapatkan nama, ukuran data, dan posisi awal LFH untuk masing-masing elemen arsip.

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

Secara umum, menjalankan file bolak-balik untuk masing-masing catatan bukanlah ide terbaik dalam hal kinerja, jadi saya akan merekomendasikan membaca semua struktur CDFH, dan kemudian membaca LFH dan data, jika perlu, tapi di sini kita memilikinya " Pemrograman yang tidak konvensional, ”jadi kami bisa.

Kode skrip lengkap
 <?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); 

Sebagai hasil dari skrip, kita harus mendapatkan informasi tentang arsip seperti berikut:

 $ 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 

Dan, secara umum, berkat data ini, kami sudah dapat mengekstrak semua atau file tertentu.

Kami melewati kompresi enkripsi dan hanya mempertimbangkan kasus ketika kami memiliki data dalam arsip sebagaimana adanya, tetapi ini tidak diperlukan untuk memahami struktur, dan bagi mereka yang ingin bingung, tidak akan sulit untuk membaca spesifikasi dan menambahkan fungsionalitas yang hilang berdasarkan seri artikel ini.

Oleh karena itu, saya pikir kita dapat mempertimbangkan siklus utama selesai. Jika Anda masih memiliki pertanyaan atau saya akan memanggil beberapa komentar di komentar untuk artikel sebelumnya, maka mungkin ada beberapa artikel tambahan.

Untuk semuanya sim.

Dan saya harap meskipun itu sedikit bagi Anda, itu masih menarik :)

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


All Articles