ZipInputStream.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. <?php
  2. namespace PhpZip\Stream;
  3. use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
  4. use PhpZip\Crypto\WinZipAesEngine;
  5. use PhpZip\Exception\Crc32Exception;
  6. use PhpZip\Exception\InvalidArgumentException;
  7. use PhpZip\Exception\RuntimeException;
  8. use PhpZip\Exception\ZipCryptoException;
  9. use PhpZip\Exception\ZipException;
  10. use PhpZip\Exception\ZipUnsupportMethodException;
  11. use PhpZip\Extra\ExtraFieldsCollection;
  12. use PhpZip\Extra\ExtraFieldsFactory;
  13. use PhpZip\Extra\Fields\ApkAlignmentExtraField;
  14. use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
  15. use PhpZip\Mapper\OffsetPositionMapper;
  16. use PhpZip\Mapper\PositionMapper;
  17. use PhpZip\Model\EndOfCentralDirectory;
  18. use PhpZip\Model\Entry\ZipSourceEntry;
  19. use PhpZip\Model\ZipEntry;
  20. use PhpZip\Model\ZipModel;
  21. use PhpZip\Util\PackUtil;
  22. use PhpZip\Util\StringUtil;
  23. use PhpZip\ZipFileInterface;
  24. /**
  25. * Read zip file
  26. *
  27. * @author Ne-Lexa alexey@nelexa.ru
  28. * @license MIT
  29. */
  30. class ZipInputStream implements ZipInputStreamInterface
  31. {
  32. /**
  33. * @var resource
  34. */
  35. protected $in;
  36. /**
  37. * @var PositionMapper
  38. */
  39. protected $mapper;
  40. /**
  41. * @var int The number of bytes in the preamble of this ZIP file.
  42. */
  43. protected $preamble = 0;
  44. /**
  45. * @var int The number of bytes in the postamble of this ZIP file.
  46. */
  47. protected $postamble = 0;
  48. /**
  49. * @var ZipModel
  50. */
  51. protected $zipModel;
  52. /**
  53. * ZipInputStream constructor.
  54. * @param resource $in
  55. */
  56. public function __construct($in)
  57. {
  58. if (!is_resource($in)) {
  59. throw new RuntimeException('$in must be resource');
  60. }
  61. $this->in = $in;
  62. $this->mapper = new PositionMapper();
  63. }
  64. /**
  65. * @return ZipModel
  66. * @throws ZipException
  67. */
  68. public function readZip()
  69. {
  70. $this->checkZipFileSignature();
  71. $endOfCentralDirectory = $this->readEndOfCentralDirectory();
  72. $entries = $this->mountCentralDirectory($endOfCentralDirectory);
  73. $this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory);
  74. return $this->zipModel;
  75. }
  76. /**
  77. * Check zip file signature
  78. *
  79. * @throws ZipException if this not .ZIP file.
  80. */
  81. protected function checkZipFileSignature()
  82. {
  83. rewind($this->in);
  84. // Constraint: A ZIP file must start with a Local File Header
  85. // or a (ZIP64) End Of Central Directory Record if it's empty.
  86. $signatureBytes = fread($this->in, 4);
  87. if (strlen($signatureBytes) < 4) {
  88. throw new ZipException("Invalid zip file.");
  89. }
  90. $signature = unpack('V', $signatureBytes)[1];
  91. if (
  92. ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
  93. && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
  94. && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
  95. ) {
  96. throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
  97. }
  98. }
  99. /**
  100. * @return EndOfCentralDirectory
  101. * @throws ZipException
  102. */
  103. protected function readEndOfCentralDirectory()
  104. {
  105. $comment = null;
  106. // Search for End of central directory record.
  107. $stats = fstat($this->in);
  108. $size = $stats['size'];
  109. $max = $size - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
  110. $min = $max >= 0xffff ? $max - 0xffff : 0;
  111. for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
  112. fseek($this->in, $endOfCentralDirRecordPos, SEEK_SET);
  113. // end of central dir signature 4 bytes (0x06054b50)
  114. if (EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($this->in, 4))[1]) {
  115. continue;
  116. }
  117. // number of this disk - 2 bytes
  118. // number of the disk with the start of the
  119. // central directory - 2 bytes
  120. // total number of entries in the central
  121. // directory on this disk - 2 bytes
  122. // total number of entries in the central
  123. // directory - 2 bytes
  124. // size of the central directory - 4 bytes
  125. // offset of start of central directory with
  126. // respect to the starting disk number - 4 bytes
  127. // ZIP file comment length - 2 bytes
  128. $data = unpack(
  129. 'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
  130. fread($this->in, 18)
  131. );
  132. if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
  133. throw new ZipException(
  134. "ZIP file spanning/splitting is not supported!"
  135. );
  136. }
  137. // .ZIP file comment (variable size)
  138. if ($data['commentLength'] > 0) {
  139. $comment = '';
  140. $offset = 0;
  141. while ($offset < $data['commentLength']) {
  142. $read = min(8192 /* chunk size */, $data['commentLength'] - $offset);
  143. $comment .= fread($this->in, $read);
  144. $offset += $read;
  145. }
  146. }
  147. $this->preamble = $endOfCentralDirRecordPos;
  148. $this->postamble = $size - ftell($this->in);
  149. // Check for ZIP64 End Of Central Directory Locator.
  150. $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
  151. fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET);
  152. // zip64 end of central dir locator
  153. // signature 4 bytes (0x07064b50)
  154. if (
  155. 0 > $endOfCentralDirLocatorPos ||
  156. ftell($this->in) === $size ||
  157. EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1]
  158. ) {
  159. // Seek and check first CFH, probably requiring an offset mapper.
  160. $offset = $endOfCentralDirRecordPos - $data['cdSize'];
  161. fseek($this->in, $offset, SEEK_SET);
  162. $offset -= $data['cdPos'];
  163. if ($offset !== 0) {
  164. $this->mapper = new OffsetPositionMapper($offset);
  165. }
  166. $entryCount = $data['cdEntries'];
  167. return new EndOfCentralDirectory($entryCount, $comment);
  168. }
  169. // number of the disk with the
  170. // start of the zip64 end of
  171. // central directory 4 bytes
  172. $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1];
  173. // relative offset of the zip64
  174. // end of central directory record 8 bytes
  175. $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8));
  176. // total number of disks 4 bytes
  177. $totalDisks = unpack('V', fread($this->in, 4))[1];
  178. if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
  179. throw new ZipException("ZIP file spanning/splitting is not supported!");
  180. }
  181. fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
  182. // zip64 end of central dir
  183. // signature 4 bytes (0x06064b50)
  184. $zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1];
  185. if (EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
  186. throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
  187. }
  188. // size of zip64 end of central
  189. // directory record 8 bytes
  190. // version made by 2 bytes
  191. // version needed to extract 2 bytes
  192. fseek($this->in, 12, SEEK_CUR);
  193. // number of this disk 4 bytes
  194. $diskNo = unpack('V', fread($this->in, 4))[1];
  195. // number of the disk with the
  196. // start of the central directory 4 bytes
  197. $cdDiskNo = unpack('V', fread($this->in, 4))[1];
  198. // total number of entries in the
  199. // central directory on this disk 8 bytes
  200. $cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8));
  201. // total number of entries in the
  202. // central directory 8 bytes
  203. $cdEntries = PackUtil::unpackLongLE(fread($this->in, 8));
  204. if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
  205. throw new ZipException("ZIP file spanning/splitting is not supported!");
  206. }
  207. if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
  208. throw new ZipException("Total Number Of Entries In The Central Directory out of range!");
  209. }
  210. // size of the central directory 8 bytes
  211. fseek($this->in, 8, SEEK_CUR);
  212. // offset of start of central
  213. // directory with respect to
  214. // the starting disk number 8 bytes
  215. $cdPos = PackUtil::unpackLongLE(fread($this->in, 8));
  216. // zip64 extensible data sector (variable size)
  217. fseek($this->in, $cdPos, SEEK_SET);
  218. $this->preamble = $zip64EndOfCentralDirectoryRecordPos;
  219. $entryCount = $cdEntries;
  220. $zip64 = true;
  221. return new EndOfCentralDirectory($entryCount, $comment, $zip64);
  222. }
  223. // Start recovering file entries from min.
  224. $this->preamble = $min;
  225. $this->postamble = $size - $min;
  226. return new EndOfCentralDirectory(0, $comment);
  227. }
  228. /**
  229. * Reads the central directory from the given seekable byte channel
  230. * and populates the internal tables with ZipEntry instances.
  231. *
  232. * The ZipEntry's will know all data that can be obtained from the
  233. * central directory alone, but not the data that requires the local
  234. * file header or additional data to be read.
  235. *
  236. * @param EndOfCentralDirectory $endOfCentralDirectory
  237. * @return ZipEntry[]
  238. * @throws ZipException
  239. */
  240. protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory)
  241. {
  242. $numEntries = $endOfCentralDirectory->getEntryCount();
  243. $entries = [];
  244. for (; $numEntries > 0; $numEntries--) {
  245. $entry = $this->readEntry();
  246. // Re-load virtual offset after ZIP64 Extended Information
  247. // Extra Field may have been parsed, map it to the real
  248. // offset and conditionally update the preamble size from it.
  249. $lfhOff = $this->mapper->map($entry->getOffset());
  250. $lfhOff = PHP_INT_SIZE === 4 ? sprintf('%u', $lfhOff) : $lfhOff;
  251. if ($lfhOff < $this->preamble) {
  252. $this->preamble = $lfhOff;
  253. }
  254. $entries[$entry->getName()] = $entry;
  255. }
  256. if (($numEntries % 0x10000) !== 0) {
  257. throw new ZipException("Expected " . abs($numEntries) .
  258. ($numEntries > 0 ? " more" : " less") .
  259. " entries in the Central Directory!");
  260. }
  261. if ($this->preamble + $this->postamble >= fstat($this->in)['size']) {
  262. $this->checkZipFileSignature();
  263. }
  264. return $entries;
  265. }
  266. /**
  267. * @return ZipEntry
  268. * @throws ZipException
  269. */
  270. public function readEntry()
  271. {
  272. // central file header signature 4 bytes (0x02014b50)
  273. $fileHeaderSig = unpack('V', fread($this->in, 4))[1];
  274. if ($fileHeaderSig !== ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG) {
  275. throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
  276. }
  277. // version made by 2 bytes
  278. // version needed to extract 2 bytes
  279. // general purpose bit flag 2 bytes
  280. // compression method 2 bytes
  281. // last mod file time 2 bytes
  282. // last mod file date 2 bytes
  283. // crc-32 4 bytes
  284. // compressed size 4 bytes
  285. // uncompressed size 4 bytes
  286. // file name length 2 bytes
  287. // extra field length 2 bytes
  288. // file comment length 2 bytes
  289. // disk number start 2 bytes
  290. // internal file attributes 2 bytes
  291. // external file attributes 4 bytes
  292. // relative offset of local header 4 bytes
  293. $data = unpack(
  294. 'vversionMadeBy/vversionNeededToExtract/vgpbf/' .
  295. 'vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
  296. 'VrawSize/vfileLength/vextraLength/vcommentLength/' .
  297. 'VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
  298. fread($this->in, 42)
  299. );
  300. // $utf8 = ($data['gpbf'] & ZipEntry::GPBF_UTF8) !== 0;
  301. // See appendix D of PKWARE's ZIP File Format Specification.
  302. $name = '';
  303. $offset = 0;
  304. while ($offset < $data['fileLength']) {
  305. $read = min(8192 /* chunk size */, $data['fileLength'] - $offset);
  306. $name .= fread($this->in, $read);
  307. $offset += $read;
  308. }
  309. $entry = new ZipSourceEntry($this);
  310. $entry->setName($name);
  311. $entry->setVersionNeededToExtract($data['versionNeededToExtract']);
  312. $entry->setPlatform($data['versionMadeBy'] >> 8);
  313. $entry->setMethod($data['rawMethod']);
  314. $entry->setGeneralPurposeBitFlags($data['gpbf']);
  315. $entry->setDosTime($data['rawTime']);
  316. $entry->setCrc($data['rawCrc']);
  317. $entry->setCompressedSize($data['rawCompressedSize']);
  318. $entry->setSize($data['rawSize']);
  319. $entry->setExternalAttributes($data['rawExternalAttributes']);
  320. $entry->setOffset($data['lfhOff']); // must be unmapped!
  321. if ($data['extraLength'] > 0) {
  322. $extra = '';
  323. $offset = 0;
  324. while ($offset < $data['extraLength']) {
  325. $read = min(8192 /* chunk size */, $data['extraLength'] - $offset);
  326. $extra .= fread($this->in, $read);
  327. $offset += $read;
  328. }
  329. $entry->setExtra($extra);
  330. }
  331. if ($data['commentLength'] > 0) {
  332. $comment = '';
  333. $offset = 0;
  334. while ($offset < $data['commentLength']) {
  335. $read = min(8192 /* chunk size */, $data['commentLength'] - $offset);
  336. $comment .= fread($this->in, $read);
  337. $offset += $read;
  338. }
  339. $entry->setComment($comment);
  340. }
  341. return $entry;
  342. }
  343. /**
  344. * @param ZipEntry $entry
  345. * @return string
  346. * @throws ZipException
  347. */
  348. public function readEntryContent(ZipEntry $entry)
  349. {
  350. if ($entry->isDirectory()) {
  351. return null;
  352. }
  353. if (!($entry instanceof ZipSourceEntry)) {
  354. throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class);
  355. }
  356. $isEncrypted = $entry->isEncrypted();
  357. if ($isEncrypted && $entry->getPassword() === null) {
  358. throw new ZipException("Can not password from entry " . $entry->getName());
  359. }
  360. $pos = $entry->getOffset();
  361. $pos = PHP_INT_SIZE === 4
  362. ? sprintf('%u', $pos) // PHP 32-Bit
  363. : $pos; // PHP 64-Bit
  364. $startPos = $pos = $this->mapper->map($pos);
  365. fseek($this->in, $startPos);
  366. // local file header signature 4 bytes (0x04034b50)
  367. if (unpack('V', fread($this->in, 4))[1] !== ZipEntry::LOCAL_FILE_HEADER_SIG) {
  368. throw new ZipException($entry->getName() . " (expected Local File Header)");
  369. }
  370. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
  371. // file name length 2 bytes
  372. // extra field length 2 bytes
  373. $data = unpack('vfileLength/vextraLength', fread($this->in, 4));
  374. $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
  375. assert(ZipEntry::UNKNOWN !== $entry->getCrc());
  376. $method = $entry->getMethod();
  377. fseek($this->in, $pos);
  378. // Get raw entry content
  379. $compressedSize = $entry->getCompressedSize();
  380. $compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize;
  381. $content = '';
  382. if ($compressedSize > 0) {
  383. $offset = 0;
  384. while ($offset < $compressedSize) {
  385. $read = min(8192 /* chunk size */, $compressedSize - $offset);
  386. $content .= fread($this->in, $read);
  387. $offset += $read;
  388. }
  389. }
  390. $skipCheckCrc = false;
  391. if ($isEncrypted) {
  392. if ($method === ZipEntry::METHOD_WINZIP_AES) {
  393. // Strong Encryption Specification - WinZip AES
  394. $winZipAesEngine = new WinZipAesEngine($entry);
  395. $content = $winZipAesEngine->decrypt($content);
  396. /**
  397. * @var WinZipAesEntryExtraField $field
  398. */
  399. $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
  400. $method = $field->getMethod();
  401. $entry->setEncryptionMethod($field->getEncryptionMethod());
  402. $skipCheckCrc = true;
  403. } else {
  404. // Traditional PKWARE Decryption
  405. $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
  406. $content = $zipCryptoEngine->decrypt($content);
  407. $entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
  408. }
  409. if (!$skipCheckCrc) {
  410. // Check CRC32 in the Local File Header or Data Descriptor.
  411. $localCrc = null;
  412. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  413. // The CRC32 is in the Data Descriptor after the compressed size.
  414. // Note the Data Descriptor's Signature is optional:
  415. // All newer apps should write it (and so does TrueVFS),
  416. // but older apps might not.
  417. fseek($this->in, $pos + $compressedSize);
  418. $localCrc = unpack('V', fread($this->in, 4))[1];
  419. if ($localCrc === ZipEntry::DATA_DESCRIPTOR_SIG) {
  420. $localCrc = unpack('V', fread($this->in, 4))[1];
  421. }
  422. } else {
  423. fseek($this->in, $startPos + 14);
  424. // The CRC32 in the Local File Header.
  425. $localCrc = sprintf('%u', fread($this->in, 4)[1]);
  426. $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
  427. }
  428. $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
  429. if ($crc != $localCrc) {
  430. throw new Crc32Exception($entry->getName(), $crc, $localCrc);
  431. }
  432. }
  433. }
  434. switch ($method) {
  435. case ZipFileInterface::METHOD_STORED:
  436. break;
  437. case ZipFileInterface::METHOD_DEFLATED:
  438. $content = gzinflate($content);
  439. break;
  440. case ZipFileInterface::METHOD_BZIP2:
  441. if (!extension_loaded('bz2')) {
  442. throw new ZipException('Extension bzip2 not install');
  443. }
  444. /** @noinspection PhpComposerExtensionStubsInspection */
  445. $content = bzdecompress($content);
  446. break;
  447. default:
  448. throw new ZipUnsupportMethodException($entry->getName() .
  449. " (compression method " . $method . " is not supported)");
  450. }
  451. if ($content === false) {
  452. throw new ZipException(sprintf(
  453. 'Failed to get the contents of the zip entry "%s"',
  454. $entry->getName()
  455. ));
  456. }
  457. if (!$skipCheckCrc) {
  458. $localCrc = crc32($content);
  459. $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
  460. $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
  461. if ($crc != $localCrc) {
  462. if ($isEncrypted) {
  463. throw new ZipCryptoException("Wrong password");
  464. }
  465. throw new Crc32Exception($entry->getName(), $crc, $localCrc);
  466. }
  467. }
  468. return $content;
  469. }
  470. /**
  471. * @return resource
  472. */
  473. public function getStream()
  474. {
  475. return $this->in;
  476. }
  477. /**
  478. * Copy the input stream of the LOC entry zip and the data into
  479. * the output stream and zip the alignment if necessary.
  480. *
  481. * @param ZipEntry $entry
  482. * @param ZipOutputStreamInterface $out
  483. * @throws ZipException
  484. */
  485. public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out)
  486. {
  487. $pos = $entry->getOffset();
  488. assert(ZipEntry::UNKNOWN !== $pos);
  489. $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos;
  490. $pos = $this->mapper->map($pos);
  491. $nameLength = strlen($entry->getName());
  492. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
  493. $sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1];
  494. if ($sourceExtraLength > 0) {
  495. // read Local File Header extra fields
  496. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, SEEK_SET);
  497. $extra = '';
  498. $offset = 0;
  499. while ($offset < $sourceExtraLength) {
  500. $read = min(8192 /* chunk size */, $sourceExtraLength - $offset);
  501. $extra .= fread($this->in, $read);
  502. $offset += $read;
  503. }
  504. $extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry);
  505. if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) {
  506. unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]);
  507. $destExtraLength = strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection));
  508. }
  509. } else {
  510. $extraFieldsCollection = new ExtraFieldsCollection();
  511. }
  512. $dataAlignmentMultiple = $this->zipModel->getZipAlign();
  513. $copyInToOutLength = $entry->getCompressedSize();
  514. fseek($this->in, $pos, SEEK_SET);
  515. if (
  516. $this->zipModel->isZipAlign() &&
  517. !$entry->isEncrypted() &&
  518. $entry->getMethod() === ZipFileInterface::METHOD_STORED
  519. ) {
  520. if (StringUtil::endsWith($entry->getName(), '.so')) {
  521. $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
  522. }
  523. $dataMinStartOffset =
  524. ftell($out->getStream()) +
  525. ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
  526. $destExtraLength +
  527. $nameLength +
  528. ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
  529. $padding =
  530. ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
  531. % $dataAlignmentMultiple;
  532. $alignExtra = new ApkAlignmentExtraField();
  533. $alignExtra->setMultiple($dataAlignmentMultiple);
  534. $alignExtra->setPadding($padding);
  535. $extraFieldsCollection->add($alignExtra);
  536. $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
  537. // copy Local File Header without extra field length
  538. // from input stream to output stream
  539. stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2);
  540. // write new extra field length (2 bytes) to output stream
  541. fwrite($out->getStream(), pack('v', strlen($extra)));
  542. // skip 2 bytes to input stream
  543. fseek($this->in, 2, SEEK_CUR);
  544. // copy name from input stream to output stream
  545. stream_copy_to_stream($this->in, $out->getStream(), $nameLength);
  546. // write extra field to output stream
  547. fwrite($out->getStream(), $extra);
  548. // skip source extraLength from input stream
  549. fseek($this->in, $sourceExtraLength, SEEK_CUR);
  550. } else {
  551. $copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength;
  552. }
  553. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  554. // crc-32 4 bytes
  555. // compressed size 4 bytes
  556. // uncompressed size 4 bytes
  557. $copyInToOutLength += 12;
  558. if ($entry->isZip64ExtensionsRequired()) {
  559. // compressed size +4 bytes
  560. // uncompressed size +4 bytes
  561. $copyInToOutLength += 8;
  562. }
  563. }
  564. // copy loc, data, data descriptor from input to output stream
  565. stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength);
  566. }
  567. /**
  568. * @param ZipEntry $entry
  569. * @param ZipOutputStreamInterface $out
  570. */
  571. public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out)
  572. {
  573. $offset = $entry->getOffset();
  574. $offset = PHP_INT_SIZE === 4 ? sprintf('%u', $offset) : $offset;
  575. $offset = $this->mapper->map($offset);
  576. $nameLength = strlen($entry->getName());
  577. fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
  578. $extraLength = unpack('v', fread($this->in, 2))[1];
  579. fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, SEEK_SET);
  580. // copy raw data from input stream to output stream
  581. stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize());
  582. }
  583. public function __destruct()
  584. {
  585. $this->close();
  586. }
  587. public function close()
  588. {
  589. if ($this->in !== null) {
  590. fclose($this->in);
  591. $this->in = null;
  592. }
  593. }
  594. }