ZipWriter.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. <?php
  2. namespace PhpZip\IO;
  3. use PhpZip\Constants\DosCodePage;
  4. use PhpZip\Constants\ZipCompressionMethod;
  5. use PhpZip\Constants\ZipConstants;
  6. use PhpZip\Constants\ZipEncryptionMethod;
  7. use PhpZip\Constants\ZipPlatform;
  8. use PhpZip\Constants\ZipVersion;
  9. use PhpZip\Exception\ZipException;
  10. use PhpZip\Exception\ZipUnsupportMethodException;
  11. use PhpZip\IO\Filter\Cipher\Traditional\PKEncryptionStreamFilter;
  12. use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter;
  13. use PhpZip\Model\Data\ZipSourceFileData;
  14. use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
  15. use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
  16. use PhpZip\Model\Extra\Fields\Zip64ExtraField;
  17. use PhpZip\Model\ZipContainer;
  18. use PhpZip\Model\ZipEntry;
  19. use PhpZip\Util\PackUtil;
  20. use PhpZip\Util\StringUtil;
  21. /**
  22. * Class ZipWriter.
  23. */
  24. class ZipWriter
  25. {
  26. /** @var int Chunk read size */
  27. const CHUNK_SIZE = 8192;
  28. /** @var ZipContainer */
  29. protected $zipContainer;
  30. /**
  31. * ZipWriter constructor.
  32. *
  33. * @param ZipContainer $container
  34. */
  35. public function __construct(ZipContainer $container)
  36. {
  37. $this->zipContainer = $container;
  38. }
  39. /**
  40. * @param resource $outStream
  41. *
  42. * @throws ZipException
  43. */
  44. public function write($outStream)
  45. {
  46. if (!\is_resource($outStream)) {
  47. throw new \InvalidArgumentException('$outStream must be resource');
  48. }
  49. $this->beforeWrite();
  50. $this->writeLocalBlock($outStream);
  51. $cdOffset = ftell($outStream);
  52. $this->writeCentralDirectoryBlock($outStream);
  53. $cdSize = ftell($outStream) - $cdOffset;
  54. $this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
  55. }
  56. protected function beforeWrite()
  57. {
  58. }
  59. /**
  60. * @param resource $outStream
  61. *
  62. * @throws ZipException
  63. */
  64. protected function writeLocalBlock($outStream)
  65. {
  66. $zipEntries = $this->zipContainer->getEntries();
  67. foreach ($zipEntries as $zipEntry) {
  68. $this->writeLocalHeader($outStream, $zipEntry);
  69. $this->writeData($outStream, $zipEntry);
  70. if ($zipEntry->isDataDescriptorEnabled()) {
  71. $this->writeDataDescriptor($outStream, $zipEntry);
  72. }
  73. }
  74. }
  75. /**
  76. * @param resource $outStream
  77. * @param ZipEntry $entry
  78. *
  79. * @throws ZipException
  80. */
  81. protected function writeLocalHeader($outStream, ZipEntry $entry)
  82. {
  83. // todo in 4.0 version move zipalign functional to ApkWriter class
  84. if ($this->zipContainer->isZipAlign()) {
  85. $this->zipAlign($outStream, $entry);
  86. }
  87. $relativeOffset = ftell($outStream);
  88. $entry->setLocalHeaderOffset($relativeOffset);
  89. if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
  90. $entry->enableDataDescriptor(true);
  91. }
  92. $dd = $entry->isDataDescriptorRequired() ||
  93. $entry->isDataDescriptorEnabled();
  94. $compressedSize = $entry->getCompressedSize();
  95. $uncompressedSize = $entry->getUncompressedSize();
  96. // todo check on 32bit system
  97. $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
  98. if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
  99. $entry->getLocalExtraFields()->add(
  100. new Zip64ExtraField($uncompressedSize, $compressedSize)
  101. );
  102. $compressedSize = ZipConstants::ZIP64_MAGIC;
  103. $uncompressedSize = ZipConstants::ZIP64_MAGIC;
  104. }
  105. $compressionMethod = $entry->getCompressionMethod();
  106. $crc = $entry->getCrc();
  107. if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
  108. /** @var WinZipAesExtraField|null $winZipAesExtra */
  109. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  110. if ($winZipAesExtra === null) {
  111. $winZipAesExtra = WinZipAesExtraField::create($entry);
  112. }
  113. if ($winZipAesExtra->isV2()) {
  114. $crc = 0;
  115. }
  116. $compressionMethod = ZipCompressionMethod::WINZIP_AES;
  117. }
  118. $extra = $this->getExtraFieldsContents($entry, true);
  119. $name = $entry->getName();
  120. $dosCharset = $entry->getCharset();
  121. if ($dosCharset !== null && !$entry->isUtf8Flag()) {
  122. $name = DosCodePage::fromUTF8($name, $dosCharset);
  123. }
  124. $nameLength = \strlen($name);
  125. $extraLength = \strlen($extra);
  126. $size = $nameLength + $extraLength;
  127. if ($size > 0xffff) {
  128. throw new ZipException(
  129. sprintf(
  130. '%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)',
  131. $entry->getName(),
  132. $size,
  133. 0xffff
  134. )
  135. );
  136. }
  137. $extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
  138. fwrite(
  139. $outStream,
  140. pack(
  141. 'VvvvVVVVvv',
  142. // local file header signature 4 bytes (0x04034b50)
  143. ZipConstants::LOCAL_FILE_HEADER,
  144. // version needed to extract 2 bytes
  145. $extractedBy,
  146. // general purpose bit flag 2 bytes
  147. $entry->getGeneralPurposeBitFlags(),
  148. // compression method 2 bytes
  149. $compressionMethod,
  150. // last mod file time 2 bytes
  151. // last mod file date 2 bytes
  152. $entry->getDosTime(),
  153. // crc-32 4 bytes
  154. $dd ? 0 : $crc,
  155. // compressed size 4 bytes
  156. $dd ? 0 : $compressedSize,
  157. // uncompressed size 4 bytes
  158. $dd ? 0 : $uncompressedSize,
  159. // file name length 2 bytes
  160. $nameLength,
  161. // extra field length 2 bytes
  162. $extraLength
  163. )
  164. );
  165. if ($nameLength > 0) {
  166. fwrite($outStream, $name);
  167. }
  168. if ($extraLength > 0) {
  169. fwrite($outStream, $extra);
  170. }
  171. }
  172. /**
  173. * @param resource $outStream
  174. * @param ZipEntry $entry
  175. *
  176. * @throws ZipException
  177. */
  178. private function zipAlign($outStream, ZipEntry $entry)
  179. {
  180. if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) {
  181. $entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID);
  182. $extra = $this->getExtraFieldsContents($entry, true);
  183. $extraLength = \strlen($extra);
  184. $name = $entry->getName();
  185. $dosCharset = $entry->getCharset();
  186. if ($dosCharset !== null && !$entry->isUtf8Flag()) {
  187. $name = DosCodePage::fromUTF8($name, $dosCharset);
  188. }
  189. $nameLength = \strlen($name);
  190. $multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES;
  191. if (StringUtil::endsWith($name, '.so')) {
  192. $multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES;
  193. }
  194. $offset = ftell($outStream);
  195. $dataMinStartOffset =
  196. $offset +
  197. ZipConstants::LFH_FILENAME_POS +
  198. $extraLength +
  199. $nameLength;
  200. $padding =
  201. ($multiple - ($dataMinStartOffset % $multiple))
  202. % $multiple;
  203. if ($padding > 0) {
  204. $dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE;
  205. $padding =
  206. ($multiple - ($dataMinStartOffset % $multiple))
  207. % $multiple;
  208. $entry->getLocalExtraFields()->add(
  209. new ApkAlignmentExtraField($multiple, $padding)
  210. );
  211. }
  212. }
  213. }
  214. /**
  215. * Merges the local file data fields of the given ZipExtraFields.
  216. *
  217. * @param ZipEntry $entry
  218. * @param bool $local
  219. *
  220. * @throws ZipException
  221. *
  222. * @return string
  223. */
  224. protected function getExtraFieldsContents(ZipEntry $entry, $local)
  225. {
  226. $local = (bool) $local;
  227. $collection = $local ?
  228. $entry->getLocalExtraFields() :
  229. $entry->getCdExtraFields();
  230. $extraData = '';
  231. foreach ($collection as $extraField) {
  232. if ($local) {
  233. $data = $extraField->packLocalFileData();
  234. } else {
  235. $data = $extraField->packCentralDirData();
  236. }
  237. $extraData .= pack(
  238. 'vv',
  239. $extraField->getHeaderId(),
  240. \strlen($data)
  241. );
  242. $extraData .= $data;
  243. }
  244. $size = \strlen($extraData);
  245. if ($size > 0xffff) {
  246. throw new ZipException(
  247. sprintf(
  248. 'Size extra out of range: %d. Extra data: %s',
  249. $size,
  250. $extraData
  251. )
  252. );
  253. }
  254. return $extraData;
  255. }
  256. /**
  257. * @param resource $outStream
  258. * @param ZipEntry $entry
  259. *
  260. * @throws ZipException
  261. */
  262. protected function writeData($outStream, ZipEntry $entry)
  263. {
  264. $zipData = $entry->getData();
  265. if ($zipData === null) {
  266. if ($entry->isDirectory()) {
  267. return;
  268. }
  269. throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName()));
  270. }
  271. // data write variants:
  272. // --------------------
  273. // * data of source zip file -> copy compressed data
  274. // * store - simple write
  275. // * store and encryption - apply encryption filter and simple write
  276. // * deflate or bzip2 - apply compression filter and simple write
  277. // * (deflate or bzip2) and encryption - create temp stream and apply
  278. // compression filter to it, then apply encryption filter to root
  279. // stream and write temp stream data.
  280. // (PHP cannot apply the filter for encryption after the compression
  281. // filter, so a temporary stream is created for the compressed data)
  282. if ($zipData instanceof ZipSourceFileData && !$this->zipContainer->hasRecompressData($entry)) {
  283. // data of source zip file -> copy compressed data
  284. $zipData->copyCompressedDataToStream($outStream);
  285. return;
  286. }
  287. $entryStream = $zipData->getDataAsStream();
  288. if (stream_get_meta_data($entryStream)['seekable']) {
  289. rewind($entryStream);
  290. }
  291. $uncompressedSize = $entry->getUncompressedSize();
  292. $posBeforeWrite = ftell($outStream);
  293. $compressionMethod = $entry->getCompressionMethod();
  294. if ($entry->isEncrypted()) {
  295. if ($compressionMethod === ZipCompressionMethod::STORED) {
  296. $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize);
  297. $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
  298. } else {
  299. $compressStream = fopen('php://temp', 'w+b');
  300. $contextFilter = $this->appendCompressionFilter($compressStream, $entry);
  301. $checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize);
  302. if ($contextFilter !== null) {
  303. stream_filter_remove($contextFilter);
  304. $contextFilter = null;
  305. }
  306. rewind($compressStream);
  307. $compressedSize = fstat($compressStream)['size'];
  308. $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize);
  309. stream_copy_to_stream($compressStream, $outStream);
  310. }
  311. } else {
  312. $contextFilter = $this->appendCompressionFilter($outStream, $entry);
  313. $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
  314. }
  315. if ($contextFilter !== null) {
  316. stream_filter_remove($contextFilter);
  317. $contextFilter = null;
  318. }
  319. // my hack {@see https://bugs.php.net/bug.php?id=49874}
  320. fseek($outStream, 0, \SEEK_END);
  321. $compressedSize = ftell($outStream) - $posBeforeWrite;
  322. $entry->setCompressedSize($compressedSize);
  323. $entry->setCrc($checksum);
  324. if (!$entry->isDataDescriptorEnabled()) {
  325. if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) {
  326. /** @var Zip64ExtraField|null $zip64ExtraLocal */
  327. $zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID);
  328. // if there is a zip64 extra record, then update it;
  329. // if not, write data to data descriptor
  330. if ($zip64ExtraLocal !== null) {
  331. $zip64ExtraLocal->setCompressedSize($compressedSize);
  332. $zip64ExtraLocal->setUncompressedSize($uncompressedSize);
  333. $posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName());
  334. fseek($outStream, $posExtra);
  335. fwrite($outStream, $this->getExtraFieldsContents($entry, true));
  336. } else {
  337. $posGPBF = $entry->getLocalHeaderOffset() + 6;
  338. $entry->enableDataDescriptor(true);
  339. fseek($outStream, $posGPBF);
  340. fwrite(
  341. $outStream,
  342. pack(
  343. 'v',
  344. // general purpose bit flag 2 bytes
  345. $entry->getGeneralPurposeBitFlags()
  346. )
  347. );
  348. }
  349. $compressedSize = ZipConstants::ZIP64_MAGIC;
  350. $uncompressedSize = ZipConstants::ZIP64_MAGIC;
  351. }
  352. $posChecksum = $entry->getLocalHeaderOffset() + 14;
  353. /** @var WinZipAesExtraField|null $winZipAesExtra */
  354. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  355. if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
  356. $checksum = 0;
  357. }
  358. fseek($outStream, $posChecksum);
  359. fwrite(
  360. $outStream,
  361. pack(
  362. 'VVV',
  363. // crc-32 4 bytes
  364. $checksum,
  365. // compressed size 4 bytes
  366. $compressedSize,
  367. // uncompressed size 4 bytes
  368. $uncompressedSize
  369. )
  370. );
  371. fseek($outStream, 0, \SEEK_END);
  372. }
  373. }
  374. /**
  375. * @param resource $inStream
  376. * @param resource $outStream
  377. * @param int $size
  378. *
  379. * @return int
  380. */
  381. private function writeAndCountChecksum($inStream, $outStream, $size)
  382. {
  383. $contextHash = hash_init('crc32b');
  384. $offset = 0;
  385. while ($offset < $size) {
  386. $read = min(self::CHUNK_SIZE, $size - $offset);
  387. $buffer = fread($inStream, $read);
  388. fwrite($outStream, $buffer);
  389. hash_update($contextHash, $buffer);
  390. $offset += $read;
  391. }
  392. return (int) hexdec(hash_final($contextHash));
  393. }
  394. /**
  395. * @param resource $outStream
  396. * @param ZipEntry $entry
  397. *
  398. * @throws ZipUnsupportMethodException
  399. *
  400. * @return resource|null
  401. */
  402. protected function appendCompressionFilter($outStream, ZipEntry $entry)
  403. {
  404. $contextCompress = null;
  405. switch ($entry->getCompressionMethod()) {
  406. case ZipCompressionMethod::DEFLATED:
  407. if (!($contextCompress = stream_filter_append(
  408. $outStream,
  409. 'zlib.deflate',
  410. \STREAM_FILTER_WRITE,
  411. ['level' => $entry->getCompressionLevel()]
  412. ))) {
  413. throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
  414. }
  415. break;
  416. case ZipCompressionMethod::BZIP2:
  417. if (!($contextCompress = stream_filter_append(
  418. $outStream,
  419. 'bzip2.compress',
  420. \STREAM_FILTER_WRITE,
  421. ['blocks' => $entry->getCompressionLevel(), 'work' => 0]
  422. ))) {
  423. throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
  424. }
  425. break;
  426. case ZipCompressionMethod::STORED:
  427. // file without compression, do nothing
  428. break;
  429. default:
  430. throw new ZipUnsupportMethodException(
  431. sprintf(
  432. '%s (compression method %d (%s) is not supported)',
  433. $entry->getName(),
  434. $entry->getCompressionMethod(),
  435. ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod())
  436. )
  437. );
  438. }
  439. return $contextCompress;
  440. }
  441. /**
  442. * @param resource $outStream
  443. * @param ZipEntry $entry
  444. * @param int $size
  445. *
  446. * @return resource|null
  447. */
  448. protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size)
  449. {
  450. $encContextFilter = null;
  451. if ($entry->isEncrypted()) {
  452. if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
  453. PKEncryptionStreamFilter::register();
  454. $cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
  455. } else {
  456. WinZipAesEncryptionStreamFilter::register();
  457. $cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
  458. }
  459. $encContextFilter = stream_filter_append(
  460. $outStream,
  461. $cipherFilterName,
  462. \STREAM_FILTER_WRITE,
  463. [
  464. 'entry' => $entry,
  465. 'size' => $size,
  466. ]
  467. );
  468. if (!$encContextFilter) {
  469. throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
  470. }
  471. }
  472. return $encContextFilter;
  473. }
  474. /**
  475. * @param resource $outStream
  476. * @param ZipEntry $entry
  477. */
  478. protected function writeDataDescriptor($outStream, ZipEntry $entry)
  479. {
  480. $crc = $entry->getCrc();
  481. /** @var WinZipAesExtraField|null $winZipAesExtra */
  482. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  483. if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
  484. $crc = 0;
  485. }
  486. fwrite(
  487. $outStream,
  488. pack(
  489. 'VV',
  490. // data descriptor signature 4 bytes (0x08074b50)
  491. ZipConstants::DATA_DESCRIPTOR,
  492. // crc-32 4 bytes
  493. $crc
  494. )
  495. );
  496. if (
  497. $entry->isZip64ExtensionsRequired() ||
  498. $entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID)
  499. ) {
  500. $dd =
  501. // compressed size 8 bytes
  502. PackUtil::packLongLE($entry->getCompressedSize()) .
  503. // uncompressed size 8 bytes
  504. PackUtil::packLongLE($entry->getUncompressedSize());
  505. } else {
  506. $dd = pack(
  507. 'VV',
  508. // compressed size 4 bytes
  509. $entry->getCompressedSize(),
  510. // uncompressed size 4 bytes
  511. $entry->getUncompressedSize()
  512. );
  513. }
  514. fwrite($outStream, $dd);
  515. }
  516. /**
  517. * @param resource $outStream
  518. *
  519. * @throws ZipException
  520. */
  521. protected function writeCentralDirectoryBlock($outStream)
  522. {
  523. foreach ($this->zipContainer->getEntries() as $outputEntry) {
  524. $this->writeCentralDirectoryHeader($outStream, $outputEntry);
  525. }
  526. }
  527. /**
  528. * Writes a Central File Header record.
  529. *
  530. * @param resource $outStream
  531. * @param ZipEntry $entry
  532. *
  533. * @throws ZipException
  534. */
  535. protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry)
  536. {
  537. $compressedSize = $entry->getCompressedSize();
  538. $uncompressedSize = $entry->getUncompressedSize();
  539. $localHeaderOffset = $entry->getLocalHeaderOffset();
  540. // todo check on 32bit system
  541. $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
  542. if (
  543. $localHeaderOffset > ZipConstants::ZIP64_MAGIC ||
  544. $compressedSize > ZipConstants::ZIP64_MAGIC ||
  545. $uncompressedSize > ZipConstants::ZIP64_MAGIC
  546. ) {
  547. $zip64ExtraField = new Zip64ExtraField();
  548. if ($uncompressedSize >= ZipConstants::ZIP64_MAGIC) {
  549. $zip64ExtraField->setUncompressedSize($uncompressedSize);
  550. $uncompressedSize = ZipConstants::ZIP64_MAGIC;
  551. }
  552. if ($compressedSize >= ZipConstants::ZIP64_MAGIC) {
  553. $zip64ExtraField->setCompressedSize($compressedSize);
  554. $compressedSize = ZipConstants::ZIP64_MAGIC;
  555. }
  556. if ($localHeaderOffset >= ZipConstants::ZIP64_MAGIC) {
  557. $zip64ExtraField->setLocalHeaderOffset($localHeaderOffset);
  558. $localHeaderOffset = ZipConstants::ZIP64_MAGIC;
  559. }
  560. $entry->getCdExtraFields()->add($zip64ExtraField);
  561. }
  562. $extra = $this->getExtraFieldsContents($entry, false);
  563. $extraLength = \strlen($extra);
  564. $name = $entry->getName();
  565. $comment = $entry->getComment();
  566. $dosCharset = $entry->getCharset();
  567. if ($dosCharset !== null && !$entry->isUtf8Flag()) {
  568. $name = DosCodePage::fromUTF8($name, $dosCharset);
  569. if ($comment) {
  570. $comment = DosCodePage::fromUTF8($comment, $dosCharset);
  571. }
  572. }
  573. $commentLength = \strlen($comment);
  574. $compressionMethod = $entry->getCompressionMethod();
  575. $crc = $entry->getCrc();
  576. /** @var WinZipAesExtraField|null $winZipAesExtra */
  577. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  578. if ($winZipAesExtra !== null) {
  579. if ($winZipAesExtra->isV2()) {
  580. $crc = 0;
  581. }
  582. $compressionMethod = ZipCompressionMethod::WINZIP_AES;
  583. }
  584. fwrite(
  585. $outStream,
  586. pack(
  587. 'VvvvvVVVVvvvvvVV',
  588. // central file header signature 4 bytes (0x02014b50)
  589. ZipConstants::CENTRAL_FILE_HEADER,
  590. // version made by 2 bytes
  591. ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
  592. // version needed to extract 2 bytes
  593. ($entry->getExtractedOS() << 8) | $entry->getExtractVersion(),
  594. // general purpose bit flag 2 bytes
  595. $entry->getGeneralPurposeBitFlags(),
  596. // compression method 2 bytes
  597. $compressionMethod,
  598. // last mod file datetime 4 bytes
  599. $entry->getDosTime(),
  600. // crc-32 4 bytes
  601. $crc,
  602. // compressed size 4 bytes
  603. $compressedSize,
  604. // uncompressed size 4 bytes
  605. $uncompressedSize,
  606. // file name length 2 bytes
  607. \strlen($name),
  608. // extra field length 2 bytes
  609. $extraLength,
  610. // file comment length 2 bytes
  611. $commentLength,
  612. // disk number start 2 bytes
  613. 0,
  614. // internal file attributes 2 bytes
  615. $entry->getInternalAttributes(),
  616. // external file attributes 4 bytes
  617. $entry->getExternalAttributes(),
  618. // relative offset of local header 4 bytes
  619. $localHeaderOffset
  620. )
  621. );
  622. // file name (variable size)
  623. fwrite($outStream, $name);
  624. if ($extraLength > 0) {
  625. // extra field (variable size)
  626. fwrite($outStream, $extra);
  627. }
  628. if ($commentLength > 0) {
  629. // file comment (variable size)
  630. fwrite($outStream, $comment);
  631. }
  632. }
  633. /**
  634. * @param resource $outStream
  635. * @param int $centralDirectoryOffset
  636. * @param int $centralDirectorySize
  637. */
  638. protected function writeEndOfCentralDirectoryBlock(
  639. $outStream,
  640. $centralDirectoryOffset,
  641. $centralDirectorySize
  642. ) {
  643. $cdEntriesCount = \count($this->zipContainer);
  644. $cdEntriesZip64 = $cdEntriesCount > 0xffff;
  645. $cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC;
  646. $cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC;
  647. $zip64Required = $cdEntriesZip64
  648. || $cdSizeZip64
  649. || $cdOffsetZip64;
  650. if ($zip64Required) {
  651. $zip64EndOfCentralDirectoryOffset = ftell($outStream);
  652. // find max software version, version needed to extract and most common platform
  653. list($softwareVersion, $versionNeededToExtract) = array_reduce(
  654. $this->zipContainer->getEntries(),
  655. static function (array $carry, ZipEntry $entry) {
  656. $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
  657. $carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF);
  658. return $carry;
  659. },
  660. [ZipVersion::v10_DEFAULT_MIN, ZipVersion::v45_ZIP64_EXT]
  661. );
  662. $createdOS = $extractedOS = ZipPlatform::OS_DOS;
  663. $versionMadeBy = ($createdOS << 8) | max($softwareVersion, ZipVersion::v45_ZIP64_EXT);
  664. $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, ZipVersion::v45_ZIP64_EXT);
  665. // write zip64 end of central directory signature
  666. fwrite(
  667. $outStream,
  668. pack(
  669. 'V',
  670. // signature 4 bytes (0x06064b50)
  671. ZipConstants::ZIP64_END_CD
  672. )
  673. );
  674. // size of zip64 end of central
  675. // directory record 8 bytes
  676. fwrite($outStream, PackUtil::packLongLE(ZipConstants::ZIP64_END_OF_CD_LEN - 12));
  677. fwrite(
  678. $outStream,
  679. pack(
  680. 'vvVV',
  681. // version made by 2 bytes
  682. $versionMadeBy & 0xFFFF,
  683. // version needed to extract 2 bytes
  684. $versionExtractedBy & 0xFFFF,
  685. // number of this disk 4 bytes
  686. 0,
  687. // number of the disk with the
  688. // start of the central directory 4 bytes
  689. 0
  690. )
  691. );
  692. fwrite(
  693. $outStream,
  694. // total number of entries in the
  695. // central directory on this disk 8 bytes
  696. PackUtil::packLongLE($cdEntriesCount) .
  697. // total number of entries in the
  698. // central directory 8 bytes
  699. PackUtil::packLongLE($cdEntriesCount) .
  700. // size of the central directory 8 bytes
  701. PackUtil::packLongLE($centralDirectorySize) .
  702. // offset of start of central
  703. // directory with respect to
  704. // the starting disk number 8 bytes
  705. PackUtil::packLongLE($centralDirectoryOffset)
  706. );
  707. // write zip64 end of central directory locator
  708. fwrite(
  709. $outStream,
  710. pack(
  711. 'VV',
  712. // zip64 end of central dir locator
  713. // signature 4 bytes (0x07064b50)
  714. ZipConstants::ZIP64_END_CD_LOC,
  715. // number of the disk with the
  716. // start of the zip64 end of
  717. // central directory 4 bytes
  718. 0
  719. ) .
  720. // relative offset of the zip64
  721. // end of central directory record 8 bytes
  722. PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset) .
  723. // total number of disks 4 bytes
  724. pack('V', 1)
  725. );
  726. }
  727. $comment = $this->zipContainer->getArchiveComment();
  728. $commentLength = $comment !== null ? \strlen($comment) : 0;
  729. fwrite(
  730. $outStream,
  731. pack(
  732. 'VvvvvVVv',
  733. // end of central dir signature 4 bytes (0x06054b50)
  734. ZipConstants::END_CD,
  735. // number of this disk 2 bytes
  736. 0,
  737. // number of the disk with the
  738. // start of the central directory 2 bytes
  739. 0,
  740. // total number of entries in the
  741. // central directory on this disk 2 bytes
  742. $cdEntriesZip64 ? 0xffff : $cdEntriesCount,
  743. // total number of entries in
  744. // the central directory 2 bytes
  745. $cdEntriesZip64 ? 0xffff : $cdEntriesCount,
  746. // size of the central directory 4 bytes
  747. $cdSizeZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectorySize,
  748. // offset of start of central
  749. // directory with respect to
  750. // the starting disk number 4 bytes
  751. $cdOffsetZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectoryOffset,
  752. // .ZIP file comment length 2 bytes
  753. $commentLength
  754. )
  755. );
  756. if ($comment !== null && $commentLength > 0) {
  757. // .ZIP file comment (variable size)
  758. fwrite($outStream, $comment);
  759. }
  760. }
  761. }