Cómo se ve el archivo zip y qué podemos hacer con él. Parte 4 - Leer el archivo

Continuación del ciclo sobre archivos Zip y PHP. Artículos anteriores: Parte 1 , Parte 2 , Parte 3

Buen día, queridos lectores.
Esta vez me gustaría presentar, probablemente, la parte final del ciclo sobre archivos Zip y PHP.

En este artículo mostraré cómo leer un archivo existente y, por ejemplo, tomaremos photos.zip de un artículo anterior . Para no repetir todos los procedimientos, usaremos el listo para usar: https://github.com/userqq/images/raw/master/photos.zip .

Ahora divaguemos por un momento y recordemos en qué consiste nuestro archivo: primero hay un conjunto de datos de archivo empaquetado, donde cada archivo empaquetado está precedido por una estructura de Encabezado de archivo local (LFH), después de todos los datos tenemos un conjunto de estructuras de Encabezado de archivo de directorio central (CDFH) - Esta es una tabla de contenido en nuestro archivo, que enumera todos los elementos y las posiciones de su desplazamiento en relación con el comienzo del archivo. Y se completa el archivo End Of Central Directory Record (EOCD): aquí está la posición del comienzo de las estructuras CDFH, su número y longitud total en bytes. Por lo tanto, el archivo debe leerse desde el final, primero para encontrar el EOCD, luego leer las estructuras CDFH y así obtener una lista de archivos en el archivo.

FYI: Y algunos formatos, como JPEG, se leen desde el principio. Por lo tanto, podemos pegar la imagen con el archivo, incluso cursi a través de cat image.jpeg archive.zip> imagearchive.jpeg , sin perder la funcionalidad. Los navegadores y las aplicaciones para ver imágenes nos mostrarán una imagen sin ningún problema. Si bien cualquier aplicación para leer archivos zip, ya sea 7z o descomprimir, podrá trabajar con calma con el archivo como archivo. Por ejemplo, aquí: https://github.com/userqq/images/blob/master/jpegarchive.jpg (Precaución, esto pesa unos 20 MB, por lo que no recomiendo abrir el tráfico desde los teléfonos o si le importa el tráfico). Por lo tanto, si conoce el alojamiento de imágenes en las que las imágenes no están recodificadas y no están recortadas, puede cargar no solo imágenes allí :) Aunque, me parece, ahora no puede encontrarlas.

Primero, definimos una función auxiliar que nos facilitará trabajar con desempaquetar (vi esta idea en una pista en uno de los repositorios para trabajar con calcetines y la modifiqué un poco para nuestro 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; } 

Ahora intentemos encontrar la estructura EOCD. Su longitud mínima, si no hay comentarios, será de 22 bytes, de los cuales los primeros 4 son una firma. Por lo tanto, primero nos movemos a la posición tamaño de archivo ($ archivo) - 22, leemos los siguientes 4 bytes, y si tenemos suerte y estos bytes son iguales a la firma (0x06054b50), entonces este es nuestro EOCD. Si no tiene suerte, byte a bit nos movemos al principio del archivo hasta que lo encontremos o terminemos, entonces, probablemente, este no sea un archivo.

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

Ahora sabemos cuántos elementos tenemos en el archivo y dónde comienza la "tabla de contenido": una lista de estructuras CDFH. Podemos iterar sobre ellos y obtener los nombres, el tamaño de los datos y la posición de inicio de LFH para cada uno de los elementos de archivo.

 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 general, ejecutar el archivo de un lado a otro para cada uno de los registros no es la mejor idea en términos de rendimiento, por lo que recomendaría leer todas las estructuras de CDFH y luego leer LFH y datos, si es necesario, pero aquí lo tenemos " Programación no convencional ", para que podamos.

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 del script, deberíamos obtener información sobre el archivo como la siguiente:

 $ 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 

Y, en general, gracias a estos datos, ya podemos extraer todo o un archivo específico.

Pasamos por alto la compresión de cifrado y consideramos solo el caso cuando tenemos los datos en el archivo tal como están, pero esto no es necesario para comprender la estructura, y para aquellos que quieran confundirse, no será difícil leer la especificación y agregar la funcionalidad faltante basada en esta serie de artículos.

Por lo tanto, creo que podemos considerar el ciclo principal terminado. Si todavía tiene preguntas o recordaré algunas menciones en los comentarios a artículos anteriores, entonces puede haber algunos artículos adicionales.

Para sim todo.

Y espero que, aunque fue un poco para ti, todavía fue interesante :)

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


All Articles