ZipReader.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the nelexa/zip package.
  5. * (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. namespace PhpZip\IO;
  10. use PhpZip\Constants\DosCodePage;
  11. use PhpZip\Constants\GeneralPurposeBitFlag;
  12. use PhpZip\Constants\ZipCompressionMethod;
  13. use PhpZip\Constants\ZipConstants;
  14. use PhpZip\Constants\ZipEncryptionMethod;
  15. use PhpZip\Constants\ZipOptions;
  16. use PhpZip\Exception\Crc32Exception;
  17. use PhpZip\Exception\InvalidArgumentException;
  18. use PhpZip\Exception\ZipException;
  19. use PhpZip\IO\Filter\Cipher\Pkware\PKDecryptionStreamFilter;
  20. use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesDecryptionStreamFilter;
  21. use PhpZip\Model\Data\ZipSourceFileData;
  22. use PhpZip\Model\EndOfCentralDirectory;
  23. use PhpZip\Model\Extra\ExtraFieldsCollection;
  24. use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
  25. use PhpZip\Model\Extra\Fields\UnrecognizedExtraField;
  26. use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
  27. use PhpZip\Model\Extra\Fields\Zip64ExtraField;
  28. use PhpZip\Model\Extra\ZipExtraDriver;
  29. use PhpZip\Model\Extra\ZipExtraField;
  30. use PhpZip\Model\ImmutableZipContainer;
  31. use PhpZip\Model\ZipEntry;
  32. /**
  33. * Zip reader.
  34. */
  35. class ZipReader
  36. {
  37. /** @var int file size */
  38. protected int $size;
  39. /** @var resource */
  40. protected $inStream;
  41. protected array $options;
  42. /**
  43. * @param resource $inStream
  44. */
  45. public function __construct($inStream, array $options = [])
  46. {
  47. if (!\is_resource($inStream)) {
  48. throw new InvalidArgumentException('Stream must be a resource');
  49. }
  50. $type = get_resource_type($inStream);
  51. if ($type !== 'stream') {
  52. throw new InvalidArgumentException("Invalid resource type {$type}.");
  53. }
  54. $meta = stream_get_meta_data($inStream);
  55. $wrapperType = $meta['wrapper_type'] ?? 'Unknown';
  56. $supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space'];
  57. if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) {
  58. throw new InvalidArgumentException(
  59. 'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode(
  60. ', ',
  61. $supportStreamWrapperTypes
  62. )
  63. );
  64. }
  65. if (
  66. $wrapperType === 'plainfile'
  67. && (
  68. $meta['stream_type'] === 'dir'
  69. || (isset($meta['uri']) && is_dir($meta['uri']))
  70. )
  71. ) {
  72. throw new InvalidArgumentException('Directory stream not supported');
  73. }
  74. $seekable = $meta['seekable'];
  75. if (!$seekable) {
  76. throw new InvalidArgumentException('Resource does not support seekable.');
  77. }
  78. $this->size = fstat($inStream)['size'];
  79. $this->inStream = $inStream;
  80. /** @noinspection AdditionOperationOnArraysInspection */
  81. $options += $this->getDefaultOptions();
  82. $this->options = $options;
  83. }
  84. protected function getDefaultOptions(): array
  85. {
  86. return [
  87. ZipOptions::CHARSET => null,
  88. ];
  89. }
  90. /**
  91. * @throws ZipException
  92. */
  93. public function read(): ImmutableZipContainer
  94. {
  95. if ($this->size < ZipConstants::END_CD_MIN_LEN) {
  96. throw new ZipException('Corrupt zip file');
  97. }
  98. $endOfCentralDirectory = $this->readEndOfCentralDirectory();
  99. $entries = $this->readCentralDirectory($endOfCentralDirectory);
  100. return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment());
  101. }
  102. public function getStreamMetaData(): array
  103. {
  104. return stream_get_meta_data($this->inStream);
  105. }
  106. /**
  107. * Read End of central directory record.
  108. *
  109. * end of central dir signature 4 bytes (0x06054b50)
  110. * number of this disk 2 bytes
  111. * number of the disk with the
  112. * start of the central directory 2 bytes
  113. * total number of entries in the
  114. * central directory on this disk 2 bytes
  115. * total number of entries in
  116. * the central directory 2 bytes
  117. * size of the central directory 4 bytes
  118. * offset of start of central
  119. * directory with respect to
  120. * the starting disk number 4 bytes
  121. * .ZIP file comment length 2 bytes
  122. * .ZIP file comment (variable size)
  123. *
  124. * @throws ZipException
  125. */
  126. protected function readEndOfCentralDirectory(): EndOfCentralDirectory
  127. {
  128. if (!$this->findEndOfCentralDirectory()) {
  129. throw new ZipException('Invalid zip file. The end of the central directory could not be found.');
  130. }
  131. $positionECD = ftell($this->inStream) - 4;
  132. $sizeECD = $this->size - ftell($this->inStream);
  133. $buffer = fread($this->inStream, $sizeECD);
  134. [
  135. 'diskNo' => $diskNo,
  136. 'cdDiskNo' => $cdDiskNo,
  137. 'cdEntriesDisk' => $cdEntriesDisk,
  138. 'cdEntries' => $cdEntries,
  139. 'cdSize' => $cdSize,
  140. 'cdPos' => $cdPos,
  141. 'commentLength' => $commentLength,
  142. ] = unpack(
  143. 'vdiskNo/vcdDiskNo/vcdEntriesDisk/'
  144. . 'vcdEntries/VcdSize/VcdPos/vcommentLength',
  145. substr($buffer, 0, 18)
  146. );
  147. if (
  148. $diskNo !== 0
  149. || $cdDiskNo !== 0
  150. || $cdEntriesDisk !== $cdEntries
  151. ) {
  152. throw new ZipException(
  153. 'ZIP file spanning/splitting is not supported!'
  154. );
  155. }
  156. $comment = null;
  157. if ($commentLength > 0) {
  158. // .ZIP file comment (variable sizeECD)
  159. $comment = substr($buffer, 18, $commentLength);
  160. }
  161. // Check for ZIP64 End Of Central Directory Locator exists.
  162. $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN;
  163. fseek($this->inStream, $zip64ECDLocatorPosition);
  164. // zip64 end of central dir locator
  165. // signature 4 bytes (0x07064b50)
  166. if (
  167. $zip64ECDLocatorPosition > 0
  168. && unpack('V', fread($this->inStream, 4))[1] === ZipConstants::ZIP64_END_CD_LOC
  169. ) {
  170. if (!$this->isZip64Support()) {
  171. throw new ZipException('ZIP64 not supported this archive.');
  172. }
  173. $positionECD = $this->findZip64ECDPosition();
  174. $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD);
  175. $endCentralDirectory->setComment($comment);
  176. } else {
  177. $endCentralDirectory = new EndOfCentralDirectory(
  178. $cdEntries,
  179. $cdPos,
  180. $cdSize,
  181. false,
  182. $comment
  183. );
  184. }
  185. return $endCentralDirectory;
  186. }
  187. protected function findEndOfCentralDirectory(): bool
  188. {
  189. $max = $this->size - ZipConstants::END_CD_MIN_LEN;
  190. $min = $max >= 0xFFFF ? $max - 0xFFFF : 0;
  191. // Search for End of central directory record.
  192. for ($position = $max; $position >= $min; $position--) {
  193. fseek($this->inStream, $position);
  194. // end of central dir signature 4 bytes (0x06054b50)
  195. if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) {
  196. continue;
  197. }
  198. return true;
  199. }
  200. return false;
  201. }
  202. /**
  203. * Read Zip64 end of central directory locator and returns
  204. * Zip64 end of central directory position.
  205. *
  206. * number of the disk with the
  207. * start of the zip64 end of
  208. * central directory 4 bytes
  209. * relative offset of the zip64
  210. * end of central directory record 8 bytes
  211. * total number of disks 4 bytes
  212. *
  213. * @throws ZipException
  214. *
  215. * @return int Zip64 End Of Central Directory position
  216. */
  217. protected function findZip64ECDPosition(): int
  218. {
  219. [
  220. 'diskNo' => $diskNo,
  221. 'zip64ECDPos' => $zip64ECDPos,
  222. 'totalDisks' => $totalDisks,
  223. ] = unpack('VdiskNo/Pzip64ECDPos/VtotalDisks', fread($this->inStream, 16));
  224. if ($diskNo !== 0 || $totalDisks > 1) {
  225. throw new ZipException('ZIP file spanning/splitting is not supported!');
  226. }
  227. return $zip64ECDPos;
  228. }
  229. /**
  230. * Read zip64 end of central directory locator and zip64 end
  231. * of central directory record.
  232. *
  233. * zip64 end of central dir
  234. * signature 4 bytes (0x06064b50)
  235. * size of zip64 end of central
  236. * directory record 8 bytes
  237. * version made by 2 bytes
  238. * version needed to extract 2 bytes
  239. * number of this disk 4 bytes
  240. * number of the disk with the
  241. * start of the central directory 4 bytes
  242. * total number of entries in the
  243. * central directory on this disk 8 bytes
  244. * total number of entries in the
  245. * central directory 8 bytes
  246. * size of the central directory 8 bytes
  247. * offset of start of central
  248. * directory with respect to
  249. * the starting disk number 8 bytes
  250. * zip64 extensible data sector (variable size)
  251. *
  252. * @throws ZipException
  253. */
  254. protected function readZip64EndOfCentralDirectory(int $zip64ECDPosition): EndOfCentralDirectory
  255. {
  256. fseek($this->inStream, $zip64ECDPosition);
  257. $buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN);
  258. if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) {
  259. throw new ZipException('Expected ZIP64 End Of Central Directory Record!');
  260. }
  261. [
  262. // 'size' => $size,
  263. // 'versionMadeBy' => $versionMadeBy,
  264. // 'extractVersion' => $extractVersion,
  265. 'diskNo' => $diskNo,
  266. 'cdDiskNo' => $cdDiskNo,
  267. 'cdEntriesDisk' => $cdEntriesDisk,
  268. 'entryCount' => $entryCount,
  269. 'cdSize' => $cdSize,
  270. 'cdPos' => $cdPos,
  271. ] = unpack(
  272. // 'Psize/vversionMadeBy/vextractVersion/'.
  273. 'VdiskNo/VcdDiskNo/PcdEntriesDisk/PentryCount/PcdSize/PcdPos',
  274. substr($buffer, 16, 40)
  275. );
  276. // $platform = ZipPlatform::fromValue(($versionMadeBy & 0xFF00) >> 8);
  277. // $softwareVersion = $versionMadeBy & 0x00FF;
  278. if ($diskNo !== 0 || $cdDiskNo !== 0 || $entryCount !== $cdEntriesDisk) {
  279. throw new ZipException('ZIP file spanning/splitting is not supported!');
  280. }
  281. if ($entryCount < 0 || $entryCount > 0x7FFFFFFF) {
  282. throw new ZipException('Total Number Of Entries In The Central Directory out of range!');
  283. }
  284. // skip zip64 extensible data sector (variable sizeEndCD)
  285. return new EndOfCentralDirectory(
  286. $entryCount,
  287. $cdPos,
  288. $cdSize,
  289. true
  290. );
  291. }
  292. /**
  293. * Reads the central directory from the given seekable byte channel
  294. * and populates the internal tables with ZipEntry instances.
  295. *
  296. * The ZipEntry's will know all data that can be obtained from the
  297. * central directory alone, but not the data that requires the local
  298. * file header or additional data to be read.
  299. *
  300. * @throws ZipException
  301. *
  302. * @return ZipEntry[]
  303. */
  304. protected function readCentralDirectory(EndOfCentralDirectory $endCD): array
  305. {
  306. $entries = [];
  307. $cdOffset = $endCD->getCdOffset();
  308. fseek($this->inStream, $cdOffset);
  309. if (!($cdStream = fopen('php://temp', 'w+b'))) {
  310. // @codeCoverageIgnoreStart
  311. throw new ZipException('A temporary resource cannot be opened for writing.');
  312. // @codeCoverageIgnoreEnd
  313. }
  314. stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize());
  315. rewind($cdStream);
  316. for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) {
  317. $zipEntry = $this->readZipEntry($cdStream);
  318. $entryName = $zipEntry->getName();
  319. /** @var UnicodePathExtraField|null $unicodePathExtraField */
  320. $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID);
  321. if ($unicodePathExtraField !== null && $unicodePathExtraField->getCrc32() === crc32($entryName)) {
  322. $unicodePath = $unicodePathExtraField->getUnicodeValue();
  323. if ($unicodePath !== '') {
  324. $unicodePath = str_replace('\\', '/', $unicodePath);
  325. if (substr_count($entryName, '/') === substr_count($unicodePath, '/')) {
  326. $entryName = $unicodePath;
  327. }
  328. }
  329. }
  330. $entries[$entryName] = $zipEntry;
  331. }
  332. return $entries;
  333. }
  334. /**
  335. * Read central directory entry.
  336. *
  337. * central file header signature 4 bytes (0x02014b50)
  338. * version made by 2 bytes
  339. * version needed to extract 2 bytes
  340. * general purpose bit flag 2 bytes
  341. * compression method 2 bytes
  342. * last mod file time 2 bytes
  343. * last mod file date 2 bytes
  344. * crc-32 4 bytes
  345. * compressed size 4 bytes
  346. * uncompressed size 4 bytes
  347. * file name length 2 bytes
  348. * extra field length 2 bytes
  349. * file comment length 2 bytes
  350. * disk number start 2 bytes
  351. * internal file attributes 2 bytes
  352. * external file attributes 4 bytes
  353. * relative offset of local header 4 bytes
  354. *
  355. * file name (variable size)
  356. * extra field (variable size)
  357. * file comment (variable size)
  358. *
  359. * @param resource $stream
  360. *
  361. * @throws ZipException
  362. */
  363. protected function readZipEntry($stream): ZipEntry
  364. {
  365. if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) {
  366. throw new ZipException('Corrupt zip file. Cannot read zip entry.');
  367. }
  368. [
  369. 'versionMadeBy' => $versionMadeBy,
  370. 'versionNeededToExtract' => $versionNeededToExtract,
  371. 'generalPurposeBitFlags' => $generalPurposeBitFlags,
  372. 'compressionMethod' => $compressionMethod,
  373. 'lastModFile' => $dosTime,
  374. 'crc' => $crc,
  375. 'compressedSize' => $compressedSize,
  376. 'uncompressedSize' => $uncompressedSize,
  377. 'fileNameLength' => $fileNameLength,
  378. 'extraFieldLength' => $extraFieldLength,
  379. 'fileCommentLength' => $fileCommentLength,
  380. 'diskNumberStart' => $diskNumberStart,
  381. 'internalFileAttributes' => $internalFileAttributes,
  382. 'externalFileAttributes' => $externalFileAttributes,
  383. 'offsetLocalHeader' => $offsetLocalHeader,
  384. ] = unpack(
  385. 'vversionMadeBy/vversionNeededToExtract/'
  386. . 'vgeneralPurposeBitFlags/vcompressionMethod/'
  387. . 'VlastModFile/Vcrc/VcompressedSize/'
  388. . 'VuncompressedSize/vfileNameLength/vextraFieldLength/'
  389. . 'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/'
  390. . 'VexternalFileAttributes/VoffsetLocalHeader',
  391. fread($stream, 42)
  392. );
  393. if ($diskNumberStart !== 0) {
  394. throw new ZipException('ZIP file spanning/splitting is not supported!');
  395. }
  396. $isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0;
  397. $name = fread($stream, $fileNameLength);
  398. $createdOS = ($versionMadeBy & 0xFF00) >> 8;
  399. $softwareVersion = $versionMadeBy & 0x00FF;
  400. $extractedOS = ($versionNeededToExtract & 0xFF00) >> 8;
  401. $extractVersion = $versionNeededToExtract & 0x00FF;
  402. $comment = null;
  403. if ($fileCommentLength > 0) {
  404. $comment = fread($stream, $fileCommentLength);
  405. }
  406. // decode code page names
  407. $fallbackCharset = null;
  408. if (!$isUtf8 && isset($this->options[ZipOptions::CHARSET])) {
  409. $charset = $this->options[ZipOptions::CHARSET];
  410. $fallbackCharset = $charset;
  411. $name = DosCodePage::toUTF8($name, $charset);
  412. if ($comment !== null) {
  413. $comment = DosCodePage::toUTF8($comment, $charset);
  414. }
  415. }
  416. $zipEntry = ZipEntry::create(
  417. $name,
  418. $createdOS,
  419. $extractedOS,
  420. $softwareVersion,
  421. $extractVersion,
  422. $compressionMethod,
  423. $generalPurposeBitFlags,
  424. $dosTime,
  425. $crc,
  426. $compressedSize,
  427. $uncompressedSize,
  428. $internalFileAttributes,
  429. $externalFileAttributes,
  430. $offsetLocalHeader,
  431. $comment,
  432. $fallbackCharset
  433. );
  434. if ($extraFieldLength > 0) {
  435. $this->parseExtraFields(
  436. fread($stream, $extraFieldLength),
  437. $zipEntry
  438. );
  439. /** @var Zip64ExtraField|null $extraZip64 */
  440. $extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID);
  441. if ($extraZip64 !== null) {
  442. $this->handleZip64Extra($extraZip64, $zipEntry);
  443. }
  444. }
  445. $this->loadLocalExtraFields($zipEntry);
  446. $this->handleExtraEncryptionFields($zipEntry);
  447. $this->handleExtraFields($zipEntry);
  448. return $zipEntry;
  449. }
  450. protected function parseExtraFields(string $buffer, ZipEntry $zipEntry, bool $local = false): ExtraFieldsCollection
  451. {
  452. $collection = $local
  453. ? $zipEntry->getLocalExtraFields()
  454. : $zipEntry->getCdExtraFields();
  455. if (!empty($buffer)) {
  456. $pos = 0;
  457. $endPos = \strlen($buffer);
  458. while ($endPos - $pos >= 4) {
  459. [
  460. 'headerId' => $headerId,
  461. 'dataSize' => $dataSize,
  462. ] = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4));
  463. $pos += 4;
  464. if ($endPos - $pos - $dataSize < 0) {
  465. break;
  466. }
  467. $bufferData = substr($buffer, $pos, $dataSize);
  468. /** @var string|ZipExtraField|null $className */
  469. $className = ZipExtraDriver::getClassNameOrNull($headerId);
  470. try {
  471. if ($className !== null) {
  472. try {
  473. $extraField = $local
  474. ? $className::unpackLocalFileData($bufferData, $zipEntry)
  475. : $className::unpackCentralDirData($bufferData, $zipEntry);
  476. } catch (\Throwable $e) {
  477. // skip errors while parsing invalid data
  478. continue;
  479. }
  480. } else {
  481. $extraField = new UnrecognizedExtraField($headerId, $bufferData);
  482. }
  483. $collection->add($extraField);
  484. } finally {
  485. $pos += $dataSize;
  486. }
  487. }
  488. }
  489. return $collection;
  490. }
  491. protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry): void
  492. {
  493. $uncompressedSize = $extraZip64->getUncompressedSize();
  494. $compressedSize = $extraZip64->getCompressedSize();
  495. $localHeaderOffset = $extraZip64->getLocalHeaderOffset();
  496. if ($uncompressedSize !== null) {
  497. $zipEntry->setUncompressedSize($uncompressedSize);
  498. }
  499. if ($compressedSize !== null) {
  500. $zipEntry->setCompressedSize($compressedSize);
  501. }
  502. if ($localHeaderOffset !== null) {
  503. $zipEntry->setLocalHeaderOffset($localHeaderOffset);
  504. }
  505. }
  506. /**
  507. * Read Local File Header.
  508. *
  509. * local file header signature 4 bytes (0x04034b50)
  510. * version needed to extract 2 bytes
  511. * general purpose bit flag 2 bytes
  512. * compression method 2 bytes
  513. * last mod file time 2 bytes
  514. * last mod file date 2 bytes
  515. * crc-32 4 bytes
  516. * compressed size 4 bytes
  517. * uncompressed size 4 bytes
  518. * file name length 2 bytes
  519. * extra field length 2 bytes
  520. * file name (variable size)
  521. * extra field (variable size)
  522. *
  523. * @throws ZipException
  524. */
  525. protected function loadLocalExtraFields(ZipEntry $entry): void
  526. {
  527. $offsetLocalHeader = $entry->getLocalHeaderOffset();
  528. fseek($this->inStream, $offsetLocalHeader);
  529. if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::LOCAL_FILE_HEADER) {
  530. throw new ZipException(sprintf('%s (expected Local File Header)', $entry->getName()));
  531. }
  532. fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS);
  533. [
  534. 'fileNameLength' => $fileNameLength,
  535. 'extraFieldLength' => $extraFieldLength,
  536. ] = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4));
  537. $offsetData = ftell($this->inStream) + $fileNameLength + $extraFieldLength;
  538. fseek($this->inStream, $fileNameLength, \SEEK_CUR);
  539. if ($extraFieldLength > 0) {
  540. $this->parseExtraFields(
  541. fread($this->inStream, $extraFieldLength),
  542. $entry,
  543. true
  544. );
  545. }
  546. $zipData = new ZipSourceFileData($this, $entry, $offsetData);
  547. $entry->setData($zipData);
  548. }
  549. /**
  550. * @throws ZipException
  551. */
  552. private function handleExtraEncryptionFields(ZipEntry $zipEntry): void
  553. {
  554. if ($zipEntry->isEncrypted()) {
  555. if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) {
  556. /** @var WinZipAesExtraField|null $extraField */
  557. $extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID);
  558. if ($extraField === null) {
  559. throw new ZipException(
  560. sprintf(
  561. 'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d',
  562. WinZipAesExtraField::HEADER_ID,
  563. $zipEntry->getCompressionMethod()
  564. )
  565. );
  566. }
  567. $zipEntry->setCompressionMethod($extraField->getCompressionMethod());
  568. $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod());
  569. } else {
  570. $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE);
  571. }
  572. }
  573. }
  574. /**
  575. * Handle extra data in zip records.
  576. *
  577. * This is a special method in which you can process ExtraField
  578. * and make changes to ZipEntry.
  579. */
  580. protected function handleExtraFields(ZipEntry $zipEntry): void
  581. {
  582. }
  583. /**
  584. * @throws ZipException
  585. * @throws Crc32Exception
  586. *
  587. * @return resource
  588. */
  589. public function getEntryStream(ZipSourceFileData $zipFileData)
  590. {
  591. $outStream = fopen('php://temp', 'w+b');
  592. $this->copyUncompressedDataToStream($zipFileData, $outStream);
  593. rewind($outStream);
  594. return $outStream;
  595. }
  596. /**
  597. * @param resource $outStream
  598. *
  599. * @throws Crc32Exception
  600. * @throws ZipException
  601. */
  602. public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream): void
  603. {
  604. if (!\is_resource($outStream)) {
  605. throw new InvalidArgumentException('outStream is not resource');
  606. }
  607. $entry = $zipFileData->getSourceEntry();
  608. // if ($entry->isDirectory()) {
  609. // throw new InvalidArgumentException('Streams not supported for directories');
  610. // }
  611. if ($entry->isStrongEncryption()) {
  612. throw new ZipException('Not support encryption zip.');
  613. }
  614. $compressionMethod = $entry->getCompressionMethod();
  615. fseek($this->inStream, $zipFileData->getOffset());
  616. $filters = [];
  617. $skipCheckCrc = false;
  618. $isEncrypted = $entry->isEncrypted();
  619. if ($isEncrypted) {
  620. if ($entry->getPassword() === null) {
  621. throw new ZipException('Can not password from entry ' . $entry->getName());
  622. }
  623. if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
  624. /** @var WinZipAesExtraField|null $winZipAesExtra */
  625. $winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID);
  626. if ($winZipAesExtra === null) {
  627. throw new ZipException(
  628. sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID)
  629. );
  630. }
  631. $compressionMethod = $winZipAesExtra->getCompressionMethod();
  632. WinZipAesDecryptionStreamFilter::register();
  633. $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME;
  634. if ($winZipAesExtra->isV2()) {
  635. $skipCheckCrc = true;
  636. }
  637. } else {
  638. PKDecryptionStreamFilter::register();
  639. $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME;
  640. }
  641. $encContextFilter = stream_filter_append(
  642. $this->inStream,
  643. $cipherFilterName,
  644. \STREAM_FILTER_READ,
  645. [
  646. 'entry' => $entry,
  647. ]
  648. );
  649. if (!$encContextFilter) {
  650. throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
  651. }
  652. $filters[] = $encContextFilter;
  653. }
  654. // hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc
  655. $pos = ftell($this->inStream);
  656. rewind($this->inStream);
  657. fseek($this->inStream, $pos);
  658. $contextDecompress = null;
  659. switch ($compressionMethod) {
  660. case ZipCompressionMethod::STORED:
  661. // file without compression, do nothing
  662. break;
  663. case ZipCompressionMethod::DEFLATED:
  664. if (!($contextDecompress = stream_filter_append(
  665. $this->inStream,
  666. 'zlib.inflate',
  667. \STREAM_FILTER_READ
  668. ))) {
  669. throw new \RuntimeException('Could not append filter "zlib.inflate" to stream');
  670. }
  671. $filters[] = $contextDecompress;
  672. break;
  673. case ZipCompressionMethod::BZIP2:
  674. if (!($contextDecompress = stream_filter_append(
  675. $this->inStream,
  676. 'bzip2.decompress',
  677. \STREAM_FILTER_READ
  678. ))) {
  679. throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream');
  680. }
  681. $filters[] = $contextDecompress;
  682. break;
  683. default:
  684. throw new ZipException(
  685. sprintf(
  686. '%s (compression method %d (%s) is not supported)',
  687. $entry->getName(),
  688. $compressionMethod,
  689. ZipCompressionMethod::getCompressionMethodName($compressionMethod)
  690. )
  691. );
  692. }
  693. $limit = $zipFileData->getUncompressedSize();
  694. $offset = 0;
  695. $chunkSize = 8192;
  696. try {
  697. if ($skipCheckCrc) {
  698. while ($offset < $limit) {
  699. $length = min($chunkSize, $limit - $offset);
  700. $buffer = fread($this->inStream, $length);
  701. if ($buffer === false) {
  702. throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
  703. }
  704. fwrite($outStream, $buffer);
  705. $offset += $length;
  706. }
  707. } else {
  708. $contextHash = hash_init('crc32b');
  709. while ($offset < $limit) {
  710. $length = min($chunkSize, $limit - $offset);
  711. $buffer = fread($this->inStream, $length);
  712. if ($buffer === false) {
  713. throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
  714. }
  715. fwrite($outStream, $buffer);
  716. hash_update($contextHash, $buffer);
  717. $offset += $length;
  718. }
  719. $expectedCrc = (int) hexdec(hash_final($contextHash));
  720. if ($expectedCrc !== $entry->getCrc()) {
  721. throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc());
  722. }
  723. }
  724. } finally {
  725. for ($i = \count($filters); $i > 0; $i--) {
  726. stream_filter_remove($filters[$i - 1]);
  727. }
  728. }
  729. }
  730. /**
  731. * @param resource $outStream
  732. */
  733. public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream): void
  734. {
  735. if ($zipData->getCompressedSize() > 0) {
  736. fseek($this->inStream, $zipData->getOffset());
  737. stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize());
  738. }
  739. }
  740. protected function isZip64Support(): bool
  741. {
  742. return \PHP_INT_SIZE === 8; // true for 64bit system
  743. }
  744. /**
  745. * @psalm-suppress InvalidPropertyAssignmentValue
  746. */
  747. public function close(): void
  748. {
  749. if (\is_resource($this->inStream)) {
  750. fclose($this->inStream);
  751. }
  752. }
  753. public function __destruct()
  754. {
  755. $this->close();
  756. }
  757. }