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\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. // we clone the container so that the changes made to
  38. // it do not affect the data in the ZipFile class
  39. $this->zipContainer = clone $container;
  40. }
  41. /**
  42. * @param resource $outStream
  43. *
  44. * @throws ZipException
  45. */
  46. public function write($outStream)
  47. {
  48. if (!\is_resource($outStream)) {
  49. throw new \InvalidArgumentException('$outStream must be resource');
  50. }
  51. $this->beforeWrite();
  52. $this->writeLocalBlock($outStream);
  53. $cdOffset = ftell($outStream);
  54. $this->writeCentralDirectoryBlock($outStream);
  55. $cdSize = ftell($outStream) - $cdOffset;
  56. $this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
  57. }
  58. protected function beforeWrite()
  59. {
  60. }
  61. /**
  62. * @param resource $outStream
  63. *
  64. * @throws ZipException
  65. */
  66. protected function writeLocalBlock($outStream)
  67. {
  68. $zipEntries = $this->zipContainer->getEntries();
  69. foreach ($zipEntries as $zipEntry) {
  70. $this->writeLocalHeader($outStream, $zipEntry);
  71. $this->writeData($outStream, $zipEntry);
  72. if ($zipEntry->isDataDescriptorEnabled()) {
  73. $this->writeDataDescriptor($outStream, $zipEntry);
  74. }
  75. }
  76. }
  77. /**
  78. * @param resource $outStream
  79. * @param ZipEntry $entry
  80. *
  81. * @throws ZipException
  82. */
  83. protected function writeLocalHeader($outStream, ZipEntry $entry)
  84. {
  85. // todo in 4.0 version move zipalign functional to ApkWriter class
  86. if ($this->zipContainer->isZipAlign()) {
  87. $this->zipAlign($outStream, $entry);
  88. }
  89. $relativeOffset = ftell($outStream);
  90. $entry->setLocalHeaderOffset($relativeOffset);
  91. if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
  92. $entry->enableDataDescriptor(true);
  93. }
  94. $dd = $entry->isDataDescriptorRequired() ||
  95. $entry->isDataDescriptorEnabled();
  96. $compressedSize = $entry->getCompressedSize();
  97. $uncompressedSize = $entry->getUncompressedSize();
  98. $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
  99. if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
  100. $entry->getLocalExtraFields()->add(
  101. new Zip64ExtraField($uncompressedSize, $compressedSize)
  102. );
  103. $compressedSize = ZipConstants::ZIP64_MAGIC;
  104. $uncompressedSize = ZipConstants::ZIP64_MAGIC;
  105. }
  106. $compressionMethod = $entry->getCompressionMethod();
  107. $crc = $entry->getCrc();
  108. if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
  109. /** @var WinZipAesExtraField|null $winZipAesExtra */
  110. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  111. if ($winZipAesExtra === null) {
  112. $winZipAesExtra = WinZipAesExtraField::create($entry);
  113. }
  114. if ($winZipAesExtra->isV2()) {
  115. $crc = 0;
  116. }
  117. $compressionMethod = ZipCompressionMethod::WINZIP_AES;
  118. }
  119. $extra = $this->getExtraFieldsContents($entry, true);
  120. $name = $entry->getName();
  121. $dosCharset = $entry->getCharset();
  122. if ($dosCharset !== null && !$entry->isUtf8Flag()) {
  123. $name = DosCodePage::fromUTF8($name, $dosCharset);
  124. }
  125. $nameLength = \strlen($name);
  126. $extraLength = \strlen($extra);
  127. $size = $nameLength + $extraLength;
  128. if ($size > 0xffff) {
  129. throw new ZipException(
  130. sprintf(
  131. '%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)',
  132. $entry->getName(),
  133. $size,
  134. 0xffff
  135. )
  136. );
  137. }
  138. $extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
  139. fwrite(
  140. $outStream,
  141. pack(
  142. 'VvvvVVVVvv',
  143. // local file header signature 4 bytes (0x04034b50)
  144. ZipConstants::LOCAL_FILE_HEADER,
  145. // version needed to extract 2 bytes
  146. $extractedBy,
  147. // general purpose bit flag 2 bytes
  148. $entry->getGeneralPurposeBitFlags(),
  149. // compression method 2 bytes
  150. $compressionMethod,
  151. // last mod file time 2 bytes
  152. // last mod file date 2 bytes
  153. $entry->getDosTime(),
  154. // crc-32 4 bytes
  155. $dd ? 0 : $crc,
  156. // compressed size 4 bytes
  157. $dd ? 0 : $compressedSize,
  158. // uncompressed size 4 bytes
  159. $dd ? 0 : $uncompressedSize,
  160. // file name length 2 bytes
  161. $nameLength,
  162. // extra field length 2 bytes
  163. $extraLength
  164. )
  165. );
  166. if ($nameLength > 0) {
  167. fwrite($outStream, $name);
  168. }
  169. if ($extraLength > 0) {
  170. fwrite($outStream, $extra);
  171. }
  172. }
  173. /**
  174. * @param resource $outStream
  175. * @param ZipEntry $entry
  176. *
  177. * @throws ZipException
  178. */
  179. private function zipAlign($outStream, ZipEntry $entry)
  180. {
  181. if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) {
  182. $entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID);
  183. $extra = $this->getExtraFieldsContents($entry, true);
  184. $extraLength = \strlen($extra);
  185. $name = $entry->getName();
  186. $dosCharset = $entry->getCharset();
  187. if ($dosCharset !== null && !$entry->isUtf8Flag()) {
  188. $name = DosCodePage::fromUTF8($name, $dosCharset);
  189. }
  190. $nameLength = \strlen($name);
  191. $multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES;
  192. if (StringUtil::endsWith($name, '.so')) {
  193. $multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES;
  194. }
  195. $offset = ftell($outStream);
  196. $dataMinStartOffset =
  197. $offset +
  198. ZipConstants::LFH_FILENAME_POS +
  199. $extraLength +
  200. $nameLength;
  201. $padding =
  202. ($multiple - ($dataMinStartOffset % $multiple))
  203. % $multiple;
  204. if ($padding > 0) {
  205. $dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE;
  206. $padding =
  207. ($multiple - ($dataMinStartOffset % $multiple))
  208. % $multiple;
  209. $entry->getLocalExtraFields()->add(
  210. new ApkAlignmentExtraField($multiple, $padding)
  211. );
  212. }
  213. }
  214. }
  215. /**
  216. * Merges the local file data fields of the given ZipExtraFields.
  217. *
  218. * @param ZipEntry $entry
  219. * @param bool $local
  220. *
  221. * @throws ZipException
  222. *
  223. * @return string
  224. */
  225. protected function getExtraFieldsContents(ZipEntry $entry, $local)
  226. {
  227. $local = (bool) $local;
  228. $collection = $local ?
  229. $entry->getLocalExtraFields() :
  230. $entry->getCdExtraFields();
  231. $extraData = '';
  232. foreach ($collection as $extraField) {
  233. if ($local) {
  234. $data = $extraField->packLocalFileData();
  235. } else {
  236. $data = $extraField->packCentralDirData();
  237. }
  238. $extraData .= pack(
  239. 'vv',
  240. $extraField->getHeaderId(),
  241. \strlen($data)
  242. );
  243. $extraData .= $data;
  244. }
  245. $size = \strlen($extraData);
  246. if ($size > 0xffff) {
  247. throw new ZipException(
  248. sprintf(
  249. 'Size extra out of range: %d. Extra data: %s',
  250. $size,
  251. $extraData
  252. )
  253. );
  254. }
  255. return $extraData;
  256. }
  257. /**
  258. * @param resource $outStream
  259. * @param ZipEntry $entry
  260. *
  261. * @throws ZipException
  262. */
  263. protected function writeData($outStream, ZipEntry $entry)
  264. {
  265. $zipData = $entry->getData();
  266. if ($zipData === null) {
  267. if ($entry->isDirectory()) {
  268. return;
  269. }
  270. throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName()));
  271. }
  272. // data write variants:
  273. // --------------------
  274. // * data of source zip file -> copy compressed data
  275. // * store - simple write
  276. // * store and encryption - apply encryption filter and simple write
  277. // * deflate or bzip2 - apply compression filter and simple write
  278. // * (deflate or bzip2) and encryption - create temp stream and apply
  279. // compression filter to it, then apply encryption filter to root
  280. // stream and write temp stream data.
  281. // (PHP cannot apply the filter for encryption after the compression
  282. // filter, so a temporary stream is created for the compressed data)
  283. if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
  284. // data of source zip file -> copy compressed data
  285. $zipData->copyCompressedDataToStream($outStream);
  286. return;
  287. }
  288. $entryStream = $zipData->getDataAsStream();
  289. if (stream_get_meta_data($entryStream)['seekable']) {
  290. rewind($entryStream);
  291. }
  292. $uncompressedSize = $entry->getUncompressedSize();
  293. $posBeforeWrite = ftell($outStream);
  294. $compressionMethod = $entry->getCompressionMethod();
  295. if ($entry->isEncrypted()) {
  296. if ($compressionMethod === ZipCompressionMethod::STORED) {
  297. $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize);
  298. $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
  299. } else {
  300. $compressStream = fopen('php://temp', 'w+b');
  301. $contextFilter = $this->appendCompressionFilter($compressStream, $entry);
  302. $checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize);
  303. if ($contextFilter !== null) {
  304. stream_filter_remove($contextFilter);
  305. $contextFilter = null;
  306. }
  307. rewind($compressStream);
  308. $compressedSize = fstat($compressStream)['size'];
  309. $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize);
  310. stream_copy_to_stream($compressStream, $outStream);
  311. }
  312. } else {
  313. $contextFilter = $this->appendCompressionFilter($outStream, $entry);
  314. $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
  315. }
  316. if ($contextFilter !== null) {
  317. stream_filter_remove($contextFilter);
  318. $contextFilter = null;
  319. }
  320. // my hack {@see https://bugs.php.net/bug.php?id=49874}
  321. fseek($outStream, 0, \SEEK_END);
  322. $compressedSize = ftell($outStream) - $posBeforeWrite;
  323. $entry->setCompressedSize($compressedSize);
  324. $entry->setCrc($checksum);
  325. if (!$entry->isDataDescriptorEnabled()) {
  326. if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) {
  327. /** @var Zip64ExtraField|null $zip64ExtraLocal */
  328. $zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID);
  329. // if there is a zip64 extra record, then update it;
  330. // if not, write data to data descriptor
  331. if ($zip64ExtraLocal !== null) {
  332. $zip64ExtraLocal->setCompressedSize($compressedSize);
  333. $zip64ExtraLocal->setUncompressedSize($uncompressedSize);
  334. $posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName());
  335. fseek($outStream, $posExtra);
  336. fwrite($outStream, $this->getExtraFieldsContents($entry, true));
  337. } else {
  338. $posGPBF = $entry->getLocalHeaderOffset() + 6;
  339. $entry->enableDataDescriptor(true);
  340. fseek($outStream, $posGPBF);
  341. fwrite(
  342. $outStream,
  343. pack(
  344. 'v',
  345. // general purpose bit flag 2 bytes
  346. $entry->getGeneralPurposeBitFlags()
  347. )
  348. );
  349. }
  350. $compressedSize = ZipConstants::ZIP64_MAGIC;
  351. $uncompressedSize = ZipConstants::ZIP64_MAGIC;
  352. }
  353. $posChecksum = $entry->getLocalHeaderOffset() + 14;
  354. /** @var WinZipAesExtraField|null $winZipAesExtra */
  355. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  356. if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
  357. $checksum = 0;
  358. }
  359. fseek($outStream, $posChecksum);
  360. fwrite(
  361. $outStream,
  362. pack(
  363. 'VVV',
  364. // crc-32 4 bytes
  365. $checksum,
  366. // compressed size 4 bytes
  367. $compressedSize,
  368. // uncompressed size 4 bytes
  369. $uncompressedSize
  370. )
  371. );
  372. fseek($outStream, 0, \SEEK_END);
  373. }
  374. }
  375. /**
  376. * @param resource $inStream
  377. * @param resource $outStream
  378. * @param int $size
  379. *
  380. * @return int
  381. */
  382. private function writeAndCountChecksum($inStream, $outStream, $size)
  383. {
  384. $contextHash = hash_init('crc32b');
  385. $offset = 0;
  386. while ($offset < $size) {
  387. $read = min(self::CHUNK_SIZE, $size - $offset);
  388. $buffer = fread($inStream, $read);
  389. fwrite($outStream, $buffer);
  390. hash_update($contextHash, $buffer);
  391. $offset += $read;
  392. }
  393. return (int) hexdec(hash_final($contextHash));
  394. }
  395. /**
  396. * @param resource $outStream
  397. * @param ZipEntry $entry
  398. *
  399. * @throws ZipUnsupportMethodException
  400. *
  401. * @return resource|null
  402. */
  403. protected function appendCompressionFilter($outStream, ZipEntry $entry)
  404. {
  405. $contextCompress = null;
  406. switch ($entry->getCompressionMethod()) {
  407. case ZipCompressionMethod::DEFLATED:
  408. if (!($contextCompress = stream_filter_append(
  409. $outStream,
  410. 'zlib.deflate',
  411. \STREAM_FILTER_WRITE,
  412. ['level' => $entry->getCompressionLevel()]
  413. ))) {
  414. throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
  415. }
  416. break;
  417. case ZipCompressionMethod::BZIP2:
  418. if (!($contextCompress = stream_filter_append(
  419. $outStream,
  420. 'bzip2.compress',
  421. \STREAM_FILTER_WRITE,
  422. ['blocks' => $entry->getCompressionLevel(), 'work' => 0]
  423. ))) {
  424. throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
  425. }
  426. break;
  427. case ZipCompressionMethod::STORED:
  428. // file without compression, do nothing
  429. break;
  430. default:
  431. throw new ZipUnsupportMethodException(
  432. sprintf(
  433. '%s (compression method %d (%s) is not supported)',
  434. $entry->getName(),
  435. $entry->getCompressionMethod(),
  436. ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod())
  437. )
  438. );
  439. }
  440. return $contextCompress;
  441. }
  442. /**
  443. * @param resource $outStream
  444. * @param ZipEntry $entry
  445. * @param int $size
  446. *
  447. * @return resource|null
  448. */
  449. protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size)
  450. {
  451. $encContextFilter = null;
  452. if ($entry->isEncrypted()) {
  453. if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
  454. PKEncryptionStreamFilter::register();
  455. $cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
  456. } else {
  457. WinZipAesEncryptionStreamFilter::register();
  458. $cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
  459. }
  460. $encContextFilter = stream_filter_append(
  461. $outStream,
  462. $cipherFilterName,
  463. \STREAM_FILTER_WRITE,
  464. [
  465. 'entry' => $entry,
  466. 'size' => $size,
  467. ]
  468. );
  469. if (!$encContextFilter) {
  470. throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
  471. }
  472. }
  473. return $encContextFilter;
  474. }
  475. /**
  476. * @param resource $outStream
  477. * @param ZipEntry $entry
  478. */
  479. protected function writeDataDescriptor($outStream, ZipEntry $entry)
  480. {
  481. $crc = $entry->getCrc();
  482. /** @var WinZipAesExtraField|null $winZipAesExtra */
  483. $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
  484. if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
  485. $crc = 0;
  486. }
  487. fwrite(
  488. $outStream,
  489. pack(
  490. 'VV',
  491. // data descriptor signature 4 bytes (0x08074b50)
  492. ZipConstants::DATA_DESCRIPTOR,
  493. // crc-32 4 bytes
  494. $crc
  495. )
  496. );
  497. if (
  498. $entry->isZip64ExtensionsRequired() ||
  499. $entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID)
  500. ) {
  501. $dd =
  502. // compressed size 8 bytes
  503. PackUtil::packLongLE($entry->getCompressedSize()) .
  504. // uncompressed size 8 bytes
  505. PackUtil::packLongLE($entry->getUncompressedSize());
  506. } else {
  507. $dd = pack(
  508. 'VV',
  509. // compressed size 4 bytes
  510. $entry->getCompressedSize(),
  511. // uncompressed size 4 bytes
  512. $entry->getUncompressedSize()
  513. );
  514. }
  515. fwrite($outStream, $dd);
  516. }
  517. /**
  518. * @param resource $outStream
  519. *
  520. * @throws ZipException
  521. */
  522. protected function writeCentralDirectoryBlock($outStream)
  523. {
  524. foreach ($this->zipContainer->getEntries() as $outputEntry) {
  525. $this->writeCentralDirectoryHeader($outStream, $outputEntry);
  526. }
  527. }
  528. /**
  529. * Writes a Central File Header record.
  530. *
  531. * @param resource $outStream
  532. * @param ZipEntry $entry
  533. *
  534. * @throws ZipException
  535. */
  536. protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry)
  537. {
  538. $compressedSize = $entry->getCompressedSize();
  539. $uncompressedSize = $entry->getUncompressedSize();
  540. $localHeaderOffset = $entry->getLocalHeaderOffset();
  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. }