ZipWriter.php 30 KB

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