Just spent the night resolving this.
There's another block of headers at the end of zip file: Central Directory.
They extend the info and, the most important, give us lengths of compressed blocks.
I find those headers by their 16-bit signatures (hope they most likely be unique and don't collide to any part of zipped contents). Then I parse them, and most of the resulting array data can be used for getting uncompressed info (this search is described in one of the @see - I've taken it as a basis, but it only works for those zips that contain lengths in pre-data header blocks).
Here is the code.
The only note is that you should use the 'extrasLen' values from the local headers that are located before corresponding compressed data pieces - because same keys from Central Directory contain their specific numbers which differ and would give you wrong ranges.
Please refer @see links, especially "brief intro", to understand how it works.
<?php
class ZipHelper
{
const METHOD_STORE = 0; // no compression
const METHOD_DEFLATED = 8; // main for all zips
const METHOD_DEFLATE64 = 9; // not supported by zlib
const LOCAL_HEAD_LENGTH = 30;
const LOCAL_HEAD_PARAMS = "Vsig/vver/vflag/vmethod/vmodTime/vmodDate/Vcrc/VcompSize/VrawSize/vnameLen/vextrasLen";
const CENTRAL_DIR_LENGTH = 46;
const CENTRAL_DIR_PARAMS = "Vsig/vverMadeBy/vverToExtract/vflag/vmethod/vmodTime/vmodDate/Vcrc/VcompSize/VrawSize/".
"vnameLen/vextrasLen/vcommLen/vdiskNumStart/vintFileAttr/VextFileAttr/VoffsetLocalHead";
/**
* @see https://mcmap.net/q/527770/-extract-a-file-from-a-zip-string
* @see https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html - brief intro
* @see https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT - full specs
*
* Uses ext-zlib
*
* @param string $zippedContent
* @return string[]
*/
public static function unzipOnAir(string $zippedContent): array
{
$result = [];
$filesInfo = self::getZippedFilesInfoFromCentralDirectoryRecords($zippedContent);
foreach ($filesInfo as $filename => $fileInfo) {
$pos = $fileInfo['offsetLocalHead'];
$head = unpack(self::LOCAL_HEAD_PARAMS, substr($zippedContent, $pos, self::LOCAL_HEAD_LENGTH));
$pos += self::LOCAL_HEAD_LENGTH + $head['nameLen'] + $head['extrasLen']; // take from $head: it differs!
$compressedData = substr($zippedContent, $pos, $fileInfo['compSize']);
switch ($fileInfo['method']) {
case self::METHOD_DEFLATED:
$unzipped = gzinflate($compressedData);
break;
case self::METHOD_STORE:
$unzipped = $compressedData;
break;
case self::METHOD_DEFLATE64:
default:
$unzipped = false;
}
$result[$filename] = $unzipped;
}
return $result;
}
/**
* @see https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html - brief intro
* @see https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT - full specs
*
* @param string $zippedContent
* @return array[]
*/
private static function getZippedFilesInfoFromCentralDirectoryRecords(string $zippedContent): array
{
$centralDirectoryRecordSignature = pack("V", '33639248'); // value is defined in ZIP standard
$out = [];
$pos = 0;
while ($pos <= strlen($zippedContent)) {
$pos = strpos($zippedContent, $centralDirectoryRecordSignature, $pos);
if ($pos === false) break;
$head = unpack(self::CENTRAL_DIR_PARAMS, substr($zippedContent, $pos, self::CENTRAL_DIR_LENGTH));
$pos += self::CENTRAL_DIR_LENGTH;
$filename = substr($zippedContent, $pos, $head['nameLen']);
$out[$filename] = $head;
$pos += $head['nameLen'] + $head['extrasLen'] + $head['commLen'];
}
return $out;
}
}