ZipInputStream.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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\ZipUnsupportMethod;
  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. * @throws RuntimeException
  56. */
  57. public function __construct($in)
  58. {
  59. if (!is_resource($in)) {
  60. throw new RuntimeException('$in must be resource');
  61. }
  62. $this->in = $in;
  63. $this->mapper = new PositionMapper();
  64. }
  65. /**
  66. * @return ZipModel
  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 (0 < $data['commentLength']) {
  139. $comment = fread($this->in, $data['commentLength']);
  140. }
  141. $this->preamble = $endOfCentralDirRecordPos;
  142. $this->postamble = $size - ftell($this->in);
  143. // Check for ZIP64 End Of Central Directory Locator.
  144. $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
  145. fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET);
  146. // zip64 end of central dir locator
  147. // signature 4 bytes (0x07064b50)
  148. if (
  149. 0 > $endOfCentralDirLocatorPos ||
  150. ftell($this->in) === $size ||
  151. EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1]
  152. ) {
  153. // Seek and check first CFH, probably requiring an offset mapper.
  154. $offset = $endOfCentralDirRecordPos - $data['cdSize'];
  155. fseek($this->in, $offset, SEEK_SET);
  156. $offset -= $data['cdPos'];
  157. if (0 !== $offset) {
  158. $this->mapper = new OffsetPositionMapper($offset);
  159. }
  160. $entryCount = $data['cdEntries'];
  161. return new EndOfCentralDirectory($entryCount, $comment);
  162. }
  163. // number of the disk with the
  164. // start of the zip64 end of
  165. // central directory 4 bytes
  166. $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1];
  167. // relative offset of the zip64
  168. // end of central directory record 8 bytes
  169. $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8));
  170. // total number of disks 4 bytes
  171. $totalDisks = unpack('V', fread($this->in, 4))[1];
  172. if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
  173. throw new ZipException("ZIP file spanning/splitting is not supported!");
  174. }
  175. fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
  176. // zip64 end of central dir
  177. // signature 4 bytes (0x06064b50)
  178. $zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1];
  179. if (EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
  180. throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
  181. }
  182. // size of zip64 end of central
  183. // directory record 8 bytes
  184. // version made by 2 bytes
  185. // version needed to extract 2 bytes
  186. fseek($this->in, 12, SEEK_CUR);
  187. // number of this disk 4 bytes
  188. $diskNo = unpack('V', fread($this->in, 4))[1];
  189. // number of the disk with the
  190. // start of the central directory 4 bytes
  191. $cdDiskNo = unpack('V', fread($this->in, 4))[1];
  192. // total number of entries in the
  193. // central directory on this disk 8 bytes
  194. $cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8));
  195. // total number of entries in the
  196. // central directory 8 bytes
  197. $cdEntries = PackUtil::unpackLongLE(fread($this->in, 8));
  198. if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
  199. throw new ZipException("ZIP file spanning/splitting is not supported!");
  200. }
  201. if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
  202. throw new ZipException("Total Number Of Entries In The Central Directory out of range!");
  203. }
  204. // size of the central directory 8 bytes
  205. fseek($this->in, 8, SEEK_CUR);
  206. // offset of start of central
  207. // directory with respect to
  208. // the starting disk number 8 bytes
  209. $cdPos = PackUtil::unpackLongLE(fread($this->in, 8));
  210. // zip64 extensible data sector (variable size)
  211. fseek($this->in, $cdPos, SEEK_SET);
  212. $this->preamble = $zip64EndOfCentralDirectoryRecordPos;
  213. $entryCount = $cdEntries;
  214. $zip64 = true;
  215. return new EndOfCentralDirectory($entryCount, $comment, $zip64);
  216. }
  217. // Start recovering file entries from min.
  218. $this->preamble = $min;
  219. $this->postamble = $size - $min;
  220. return new EndOfCentralDirectory(0, $comment);
  221. }
  222. /**
  223. * Reads the central directory from the given seekable byte channel
  224. * and populates the internal tables with ZipEntry instances.
  225. *
  226. * The ZipEntry's will know all data that can be obtained from the
  227. * central directory alone, but not the data that requires the local
  228. * file header or additional data to be read.
  229. *
  230. * @param EndOfCentralDirectory $endOfCentralDirectory
  231. * @return ZipEntry[]
  232. * @throws ZipException
  233. */
  234. protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory)
  235. {
  236. $numEntries = $endOfCentralDirectory->getEntryCount();
  237. $entries = [];
  238. for (; $numEntries > 0; $numEntries--) {
  239. $entry = $this->readEntry();
  240. // Re-load virtual offset after ZIP64 Extended Information
  241. // Extra Field may have been parsed, map it to the real
  242. // offset and conditionally update the preamble size from it.
  243. $lfhOff = $this->mapper->map($entry->getOffset());
  244. $lfhOff = PHP_INT_SIZE === 4 ? sprintf('%u', $lfhOff) : $lfhOff;
  245. if ($lfhOff < $this->preamble) {
  246. $this->preamble = $lfhOff;
  247. }
  248. $entries[$entry->getName()] = $entry;
  249. }
  250. if (0 !== $numEntries % 0x10000) {
  251. throw new ZipException("Expected " . abs($numEntries) .
  252. ($numEntries > 0 ? " more" : " less") .
  253. " entries in the Central Directory!");
  254. }
  255. if ($this->preamble + $this->postamble >= fstat($this->in)['size']) {
  256. assert(0 === $numEntries);
  257. $this->checkZipFileSignature();
  258. }
  259. return $entries;
  260. }
  261. /**
  262. * @return ZipEntry
  263. * @throws InvalidArgumentException
  264. */
  265. public function readEntry()
  266. {
  267. // central file header signature 4 bytes (0x02014b50)
  268. $fileHeaderSig = unpack('V', fread($this->in, 4))[1];
  269. if (ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) {
  270. throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
  271. }
  272. // version made by 2 bytes
  273. // version needed to extract 2 bytes
  274. // general purpose bit flag 2 bytes
  275. // compression method 2 bytes
  276. // last mod file time 2 bytes
  277. // last mod file date 2 bytes
  278. // crc-32 4 bytes
  279. // compressed size 4 bytes
  280. // uncompressed size 4 bytes
  281. // file name length 2 bytes
  282. // extra field length 2 bytes
  283. // file comment length 2 bytes
  284. // disk number start 2 bytes
  285. // internal file attributes 2 bytes
  286. // external file attributes 4 bytes
  287. // relative offset of local header 4 bytes
  288. $data = unpack(
  289. 'vversionMadeBy/vversionNeededToExtract/vgpbf/' .
  290. 'vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
  291. 'VrawSize/vfileLength/vextraLength/vcommentLength/' .
  292. 'VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
  293. fread($this->in, 42)
  294. );
  295. // $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
  296. // See appendix D of PKWARE's ZIP File Format Specification.
  297. $name = fread($this->in, $data['fileLength']);
  298. $entry = new ZipSourceEntry($this);
  299. $entry->setName($name);
  300. $entry->setVersionNeededToExtract($data['versionNeededToExtract']);
  301. $entry->setPlatform($data['versionMadeBy'] >> 8);
  302. $entry->setMethod($data['rawMethod']);
  303. $entry->setGeneralPurposeBitFlags($data['gpbf']);
  304. $entry->setDosTime($data['rawTime']);
  305. $entry->setCrc($data['rawCrc']);
  306. $entry->setCompressedSize($data['rawCompressedSize']);
  307. $entry->setSize($data['rawSize']);
  308. $entry->setExternalAttributes($data['rawExternalAttributes']);
  309. $entry->setOffset($data['lfhOff']); // must be unmapped!
  310. if (0 < $data['extraLength']) {
  311. $entry->setExtra(fread($this->in, $data['extraLength']));
  312. }
  313. if (0 < $data['commentLength']) {
  314. $entry->setComment(fread($this->in, $data['commentLength']));
  315. }
  316. return $entry;
  317. }
  318. /**
  319. * @param ZipEntry $entry
  320. * @return string
  321. * @throws ZipException
  322. */
  323. public function readEntryContent(ZipEntry $entry)
  324. {
  325. if ($entry->isDirectory()) {
  326. return null;
  327. }
  328. if (!($entry instanceof ZipSourceEntry)) {
  329. throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class);
  330. }
  331. $isEncrypted = $entry->isEncrypted();
  332. if ($isEncrypted && null === $entry->getPassword()) {
  333. throw new ZipException("Can not password from entry " . $entry->getName());
  334. }
  335. $pos = $entry->getOffset();
  336. assert(ZipEntry::UNKNOWN !== $pos);
  337. $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos;
  338. $startPos = $pos = $this->mapper->map($pos);
  339. fseek($this->in, $startPos);
  340. // local file header signature 4 bytes (0x04034b50)
  341. if (ZipEntry::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->in, 4))[1]) {
  342. throw new ZipException($entry->getName() . " (expected Local File Header)");
  343. }
  344. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
  345. // file name length 2 bytes
  346. // extra field length 2 bytes
  347. $data = unpack('vfileLength/vextraLength', fread($this->in, 4));
  348. $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
  349. assert(ZipEntry::UNKNOWN !== $entry->getCrc());
  350. $method = $entry->getMethod();
  351. fseek($this->in, $pos);
  352. // Get raw entry content
  353. $compressedSize = $entry->getCompressedSize();
  354. $compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize;
  355. if ($compressedSize > 0) {
  356. $content = fread($this->in, $compressedSize);
  357. } else {
  358. $content = '';
  359. }
  360. $skipCheckCrc = false;
  361. if ($isEncrypted) {
  362. if (ZipEntry::METHOD_WINZIP_AES === $method) {
  363. // Strong Encryption Specification - WinZip AES
  364. $winZipAesEngine = new WinZipAesEngine($entry);
  365. $content = $winZipAesEngine->decrypt($content);
  366. /**
  367. * @var WinZipAesEntryExtraField $field
  368. */
  369. $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
  370. $method = $field->getMethod();
  371. $entry->setEncryptionMethod($field->getEncryptionMethod());
  372. $skipCheckCrc = true;
  373. } else {
  374. // Traditional PKWARE Decryption
  375. $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
  376. $content = $zipCryptoEngine->decrypt($content);
  377. $entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
  378. }
  379. if (!$skipCheckCrc) {
  380. // Check CRC32 in the Local File Header or Data Descriptor.
  381. $localCrc = null;
  382. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  383. // The CRC32 is in the Data Descriptor after the compressed size.
  384. // Note the Data Descriptor's Signature is optional:
  385. // All newer apps should write it (and so does TrueVFS),
  386. // but older apps might not.
  387. fseek($this->in, $pos + $compressedSize);
  388. $localCrc = unpack('V', fread($this->in, 4))[1];
  389. if (ZipEntry::DATA_DESCRIPTOR_SIG === $localCrc) {
  390. $localCrc = unpack('V', fread($this->in, 4))[1];
  391. }
  392. } else {
  393. fseek($this->in, $startPos + 14);
  394. // The CRC32 in the Local File Header.
  395. $localCrc = sprintf('%u', fread($this->in, 4)[1]);
  396. $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
  397. }
  398. $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
  399. if ($crc != $localCrc) {
  400. throw new Crc32Exception($entry->getName(), $crc, $localCrc);
  401. }
  402. }
  403. }
  404. switch ($method) {
  405. case ZipFileInterface::METHOD_STORED:
  406. break;
  407. case ZipFileInterface::METHOD_DEFLATED:
  408. $content = gzinflate($content);
  409. break;
  410. case ZipFileInterface::METHOD_BZIP2:
  411. if (!extension_loaded('bz2')) {
  412. throw new ZipException('Extension bzip2 not install');
  413. }
  414. $content = bzdecompress($content);
  415. break;
  416. default:
  417. throw new ZipUnsupportMethod($entry->getName() .
  418. " (compression method " . $method . " is not supported)");
  419. }
  420. if (!$skipCheckCrc) {
  421. $localCrc = crc32($content);
  422. $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
  423. $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
  424. if ($crc != $localCrc) {
  425. if ($isEncrypted) {
  426. throw new ZipCryptoException("Wrong password");
  427. }
  428. throw new Crc32Exception($entry->getName(), $crc, $localCrc);
  429. }
  430. }
  431. return $content;
  432. }
  433. /**
  434. * @return resource
  435. */
  436. public function getStream()
  437. {
  438. return $this->in;
  439. }
  440. /**
  441. * Copy the input stream of the LOC entry zip and the data into
  442. * the output stream and zip the alignment if necessary.
  443. *
  444. * @param ZipEntry $entry
  445. * @param ZipOutputStreamInterface $out
  446. */
  447. public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out)
  448. {
  449. $pos = $entry->getOffset();
  450. assert(ZipEntry::UNKNOWN !== $pos);
  451. $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos;
  452. $pos = $this->mapper->map($pos);
  453. $nameLength = strlen($entry->getName());
  454. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
  455. $sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1];
  456. if ($sourceExtraLength > 0) {
  457. // read Local File Header extra fields
  458. fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, SEEK_SET);
  459. $extra = fread($this->in, $sourceExtraLength);
  460. $extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry);
  461. if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) {
  462. unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]);
  463. $destExtraLength = strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection));
  464. }
  465. } else {
  466. $extraFieldsCollection = new ExtraFieldsCollection();
  467. }
  468. $dataAlignmentMultiple = $this->zipModel->getZipAlign();
  469. $copyInToOutLength = $entry->getCompressedSize();
  470. fseek($this->in, $pos, SEEK_SET);
  471. if (
  472. $this->zipModel->isZipAlign() &&
  473. !$entry->isEncrypted() &&
  474. $entry->getMethod() === ZipFileInterface::METHOD_STORED
  475. ) {
  476. if (StringUtil::endsWith($entry->getName(), '.so')) {
  477. $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
  478. }
  479. $dataMinStartOffset =
  480. ftell($out->getStream()) +
  481. ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
  482. $destExtraLength +
  483. $nameLength +
  484. ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
  485. $padding =
  486. ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
  487. % $dataAlignmentMultiple;
  488. $alignExtra = new ApkAlignmentExtraField();
  489. $alignExtra->setMultiple($dataAlignmentMultiple);
  490. $alignExtra->setPadding($padding);
  491. $extraFieldsCollection->add($alignExtra);
  492. $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
  493. // copy Local File Header without extra field length
  494. // from input stream to output stream
  495. stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2);
  496. // write new extra field length (2 bytes) to output stream
  497. fwrite($out->getStream(), pack('v', strlen($extra)));
  498. // skip 2 bytes to input stream
  499. fseek($this->in, 2, SEEK_CUR);
  500. // copy name from input stream to output stream
  501. stream_copy_to_stream($this->in, $out->getStream(), $nameLength);
  502. // write extra field to output stream
  503. fwrite($out->getStream(), $extra);
  504. // skip source extraLength from input stream
  505. fseek($this->in, $sourceExtraLength, SEEK_CUR);
  506. } else {
  507. $copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength;
  508. ;
  509. }
  510. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  511. // crc-32 4 bytes
  512. // compressed size 4 bytes
  513. // uncompressed size 4 bytes
  514. $copyInToOutLength += 12;
  515. if ($entry->isZip64ExtensionsRequired()) {
  516. // compressed size +4 bytes
  517. // uncompressed size +4 bytes
  518. $copyInToOutLength += 8;
  519. }
  520. }
  521. // copy loc, data, data descriptor from input to output stream
  522. stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength);
  523. }
  524. /**
  525. * @param ZipEntry $entry
  526. * @param ZipOutputStreamInterface $out
  527. */
  528. public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out)
  529. {
  530. $offset = $entry->getOffset();
  531. $offset = PHP_INT_SIZE === 4 ? sprintf('%u', $offset) : $offset;
  532. $offset = $this->mapper->map($offset);
  533. $nameLength = strlen($entry->getName());
  534. fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
  535. $extraLength = unpack('v', fread($this->in, 2))[1];
  536. fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, SEEK_SET);
  537. // copy raw data from input stream to output stream
  538. stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize());
  539. }
  540. public function __destruct()
  541. {
  542. $this->close();
  543. }
  544. public function close()
  545. {
  546. if ($this->in != null) {
  547. fclose($this->in);
  548. $this->in = null;
  549. }
  550. }
  551. }