ZipReader.php 29 KB

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