ZipOutputStream.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. <?php
  2. namespace PhpZip\Stream;
  3. use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
  4. use PhpZip\Crypto\WinZipAesEngine;
  5. use PhpZip\Exception\InvalidArgumentException;
  6. use PhpZip\Exception\RuntimeException;
  7. use PhpZip\Exception\ZipException;
  8. use PhpZip\Extra\ExtraFieldsFactory;
  9. use PhpZip\Extra\Fields\ApkAlignmentExtraField;
  10. use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
  11. use PhpZip\Extra\Fields\Zip64ExtraField;
  12. use PhpZip\Model\EndOfCentralDirectory;
  13. use PhpZip\Model\Entry\OutputOffsetEntry;
  14. use PhpZip\Model\Entry\ZipChangesEntry;
  15. use PhpZip\Model\Entry\ZipSourceEntry;
  16. use PhpZip\Model\ZipEntry;
  17. use PhpZip\Model\ZipModel;
  18. use PhpZip\Util\PackUtil;
  19. use PhpZip\Util\StringUtil;
  20. use PhpZip\ZipFile;
  21. /**
  22. * Write zip file.
  23. *
  24. * @author Ne-Lexa alexey@nelexa.ru
  25. * @license MIT
  26. */
  27. class ZipOutputStream implements ZipOutputStreamInterface
  28. {
  29. /** @var resource */
  30. protected $out;
  31. /** @var ZipModel */
  32. protected $zipModel;
  33. /**
  34. * ZipOutputStream constructor.
  35. *
  36. * @param resource $out
  37. * @param ZipModel $zipModel
  38. */
  39. public function __construct($out, ZipModel $zipModel)
  40. {
  41. if (!\is_resource($out)) {
  42. throw new InvalidArgumentException('$out must be resource');
  43. }
  44. $this->out = $out;
  45. $this->zipModel = $zipModel;
  46. }
  47. /**
  48. * @throws ZipException
  49. */
  50. public function writeZip()
  51. {
  52. $entries = $this->zipModel->getEntries();
  53. $outPosEntries = [];
  54. foreach ($entries as $entry) {
  55. $outPosEntries[] = new OutputOffsetEntry(ftell($this->out), $entry);
  56. $this->writeEntry($entry);
  57. }
  58. $centralDirectoryOffset = ftell($this->out);
  59. foreach ($outPosEntries as $outputEntry) {
  60. $this->writeCentralDirectoryHeader($outputEntry);
  61. }
  62. $this->writeEndOfCentralDirectoryRecord($centralDirectoryOffset);
  63. }
  64. /**
  65. * @param ZipEntry $entry
  66. *
  67. * @throws ZipException
  68. */
  69. public function writeEntry(ZipEntry $entry)
  70. {
  71. if ($entry instanceof ZipSourceEntry) {
  72. $entry->getInputStream()->copyEntry($entry, $this);
  73. return;
  74. }
  75. $entryContent = $this->entryCommitChangesAndReturnContent($entry);
  76. $offset = ftell($this->out);
  77. $compressedSize = $entry->getCompressedSize();
  78. $extra = $entry->getExtra();
  79. $nameLength = \strlen($entry->getName());
  80. $extraLength = \strlen($extra);
  81. // zip align
  82. if (
  83. $this->zipModel->isZipAlign() &&
  84. !$entry->isEncrypted() &&
  85. $entry->getMethod() === ZipFile::METHOD_STORED
  86. ) {
  87. $dataAlignmentMultiple = $this->zipModel->getZipAlign();
  88. if (StringUtil::endsWith($entry->getName(), '.so')) {
  89. $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
  90. }
  91. $dataMinStartOffset =
  92. $offset +
  93. ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
  94. $extraLength +
  95. $nameLength +
  96. ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
  97. $padding =
  98. ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
  99. % $dataAlignmentMultiple;
  100. $alignExtra = new ApkAlignmentExtraField();
  101. $alignExtra->setMultiple($dataAlignmentMultiple);
  102. $alignExtra->setPadding($padding);
  103. $extraFieldsCollection = clone $entry->getExtraFieldsCollection();
  104. $extraFieldsCollection->add($alignExtra);
  105. $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
  106. $extraLength = \strlen($extra);
  107. }
  108. $size = $nameLength + $extraLength;
  109. if ($size > 0xffff) {
  110. throw new ZipException(
  111. $entry->getName() . ' (the total size of ' . $size .
  112. ' bytes for the name, extra fields and comment ' .
  113. 'exceeds the maximum size of ' . 0xffff . ' bytes)'
  114. );
  115. }
  116. $dd = $entry->isDataDescriptorRequired();
  117. fwrite(
  118. $this->out,
  119. pack(
  120. 'VvvvVVVVvv',
  121. // local file header signature 4 bytes (0x04034b50)
  122. ZipEntry::LOCAL_FILE_HEADER_SIG,
  123. // version needed to extract 2 bytes
  124. ($entry->getExtractedOS() << 8) | $entry->getVersionNeededToExtract(),
  125. // general purpose bit flag 2 bytes
  126. $entry->getGeneralPurposeBitFlags(),
  127. // compression method 2 bytes
  128. $entry->getMethod(),
  129. // last mod file time 2 bytes
  130. // last mod file date 2 bytes
  131. $entry->getDosTime(),
  132. // crc-32 4 bytes
  133. $dd ? 0 : $entry->getCrc(),
  134. // compressed size 4 bytes
  135. $dd ? 0 : $entry->getCompressedSize(),
  136. // uncompressed size 4 bytes
  137. $dd ? 0 : $entry->getSize(),
  138. // file name length 2 bytes
  139. $nameLength,
  140. // extra field length 2 bytes
  141. $extraLength
  142. )
  143. );
  144. if ($nameLength > 0) {
  145. fwrite($this->out, $entry->getName());
  146. }
  147. if ($extraLength > 0) {
  148. fwrite($this->out, $extra);
  149. }
  150. if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) {
  151. $entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this);
  152. } elseif ($entryContent !== null) {
  153. fwrite($this->out, $entryContent);
  154. }
  155. if ($entry->getCrc() === ZipEntry::UNKNOWN) {
  156. throw new ZipException(sprintf('No crc for entry %s', $entry->getName()));
  157. }
  158. if ($entry->getSize() === ZipEntry::UNKNOWN) {
  159. throw new ZipException(sprintf('No uncompressed size for entry %s', $entry->getName()));
  160. }
  161. if ($entry->getCompressedSize() === ZipEntry::UNKNOWN) {
  162. throw new ZipException(sprintf('No compressed size for entry %s', $entry->getName()));
  163. }
  164. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  165. // data descriptor signature 4 bytes (0x08074b50)
  166. // crc-32 4 bytes
  167. fwrite($this->out, pack('VV', ZipEntry::DATA_DESCRIPTOR_SIG, $entry->getCrc()));
  168. // compressed size 4 or 8 bytes
  169. // uncompressed size 4 or 8 bytes
  170. if ($entry->isZip64ExtensionsRequired()) {
  171. fwrite($this->out, PackUtil::packLongLE($compressedSize));
  172. fwrite($this->out, PackUtil::packLongLE($entry->getSize()));
  173. } else {
  174. fwrite($this->out, pack('VV', $entry->getCompressedSize(), $entry->getSize()));
  175. }
  176. } elseif ($compressedSize !== $entry->getCompressedSize()) {
  177. throw new ZipException(
  178. $entry->getName() . ' (expected compressed entry size of '
  179. . $entry->getCompressedSize() . ' bytes, ' .
  180. 'but is actually ' . $compressedSize . ' bytes)'
  181. );
  182. }
  183. }
  184. /**
  185. * @param ZipEntry $entry
  186. *
  187. * @throws ZipException
  188. *
  189. * @return string|null
  190. */
  191. protected function entryCommitChangesAndReturnContent(ZipEntry $entry)
  192. {
  193. if ($entry->getCreatedOS() === ZipEntry::UNKNOWN) {
  194. $entry->setCreatedOS(ZipEntry::PLATFORM_UNIX);
  195. }
  196. if ($entry->getSoftwareVersion() === ZipEntry::UNKNOWN) {
  197. $entry->setSoftwareVersion(63);
  198. }
  199. if ($entry->getExtractedOS() === ZipEntry::UNKNOWN) {
  200. $entry->setExtractedOS(ZipEntry::PLATFORM_UNIX);
  201. }
  202. if ($entry->getTime() === ZipEntry::UNKNOWN) {
  203. $entry->setTime(time());
  204. }
  205. $method = $entry->getMethod();
  206. $encrypted = $entry->isEncrypted();
  207. // See appendix D of PKWARE's ZIP File Format Specification.
  208. $utf8 = true;
  209. if ($encrypted && $entry->getPassword() === null) {
  210. throw new ZipException(sprintf('Password not set for entry %s', $entry->getName()));
  211. }
  212. // Compose General Purpose Bit Flag.
  213. $general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0)
  214. | ($entry->isDataDescriptorRequired() ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0)
  215. | ($utf8 ? ZipEntry::GPBF_UTF8 : 0);
  216. $entryContent = null;
  217. $extraFieldsCollection = $entry->getExtraFieldsCollection();
  218. if (!($entry instanceof ZipChangesEntry && !$entry->isChangedContent())) {
  219. $entryContent = $entry->getEntryContent();
  220. if ($entryContent !== null) {
  221. $entry->setSize(\strlen($entryContent));
  222. $entry->setCrc(crc32($entryContent));
  223. if ($encrypted && $method === ZipEntry::METHOD_WINZIP_AES) {
  224. /**
  225. * @var WinZipAesEntryExtraField $field
  226. */
  227. $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId());
  228. if ($field !== null) {
  229. $method = $field->getMethod();
  230. }
  231. }
  232. switch ($method) {
  233. case ZipFile::METHOD_STORED:
  234. break;
  235. case ZipFile::METHOD_DEFLATED:
  236. $entryContent = gzdeflate($entryContent, $entry->getCompressionLevel());
  237. break;
  238. case ZipFile::METHOD_BZIP2:
  239. $compressionLevel = $entry->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
  240. ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION :
  241. $entry->getCompressionLevel();
  242. /** @noinspection PhpComposerExtensionStubsInspection */
  243. $entryContent = bzcompress($entryContent, $compressionLevel);
  244. if (\is_int($entryContent)) {
  245. throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
  246. }
  247. break;
  248. case ZipEntry::UNKNOWN:
  249. $entryContent = $this->determineBestCompressionMethod($entry, $entryContent);
  250. $method = $entry->getMethod();
  251. break;
  252. default:
  253. throw new ZipException($entry->getName() . ' (unsupported compression method ' . $method . ')');
  254. }
  255. if ($method === ZipFile::METHOD_DEFLATED) {
  256. $bit1 = false;
  257. $bit2 = false;
  258. switch ($entry->getCompressionLevel()) {
  259. case ZipFile::LEVEL_BEST_COMPRESSION:
  260. $bit1 = true;
  261. break;
  262. case ZipFile::LEVEL_FAST:
  263. $bit2 = true;
  264. break;
  265. case ZipFile::LEVEL_SUPER_FAST:
  266. $bit1 = true;
  267. $bit2 = true;
  268. break;
  269. }
  270. $general |= ($bit1 ? ZipEntry::GPBF_COMPRESSION_FLAG1 : 0);
  271. $general |= ($bit2 ? ZipEntry::GPBF_COMPRESSION_FLAG2 : 0);
  272. }
  273. if ($encrypted) {
  274. if (\in_array(
  275. $entry->getEncryptionMethod(),
  276. [
  277. ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128,
  278. ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192,
  279. ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256,
  280. ],
  281. true
  282. )) {
  283. $keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod(
  284. $entry->getEncryptionMethod()
  285. ); // size bits
  286. $field = ExtraFieldsFactory::createWinZipAesEntryExtra();
  287. $field->setKeyStrength($keyStrength);
  288. $field->setMethod($method);
  289. $size = $entry->getSize();
  290. if ($size >= 20 && $method !== ZipFile::METHOD_BZIP2) {
  291. $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
  292. } else {
  293. $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
  294. $entry->setCrc(0);
  295. }
  296. $extraFieldsCollection->add($field);
  297. $entry->setMethod(ZipEntry::METHOD_WINZIP_AES);
  298. $winZipAesEngine = new WinZipAesEngine($entry);
  299. $entryContent = $winZipAesEngine->encrypt($entryContent);
  300. } elseif ($entry->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
  301. $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
  302. $entryContent = $zipCryptoEngine->encrypt($entryContent);
  303. }
  304. }
  305. $compressedSize = \strlen($entryContent);
  306. $entry->setCompressedSize($compressedSize);
  307. }
  308. }
  309. // Commit changes.
  310. $entry->setGeneralPurposeBitFlags($general);
  311. if ($entry->isZip64ExtensionsRequired()) {
  312. $extraFieldsCollection->add(ExtraFieldsFactory::createZip64Extra($entry));
  313. } elseif ($extraFieldsCollection->has(Zip64ExtraField::getHeaderId())) {
  314. $extraFieldsCollection->remove(Zip64ExtraField::getHeaderId());
  315. }
  316. return $entryContent;
  317. }
  318. /**
  319. * @param ZipEntry $entry
  320. * @param string $content
  321. *
  322. * @throws ZipException
  323. *
  324. * @return string
  325. */
  326. protected function determineBestCompressionMethod(ZipEntry $entry, $content)
  327. {
  328. if ($content !== null) {
  329. $entryContent = gzdeflate($content, $entry->getCompressionLevel());
  330. if (\strlen($entryContent) < \strlen($content)) {
  331. $entry->setMethod(ZipFile::METHOD_DEFLATED);
  332. return $entryContent;
  333. }
  334. $entry->setMethod(ZipFile::METHOD_STORED);
  335. }
  336. return $content;
  337. }
  338. /**
  339. * Writes a Central File Header record.
  340. *
  341. * @param OutputOffsetEntry $outEntry
  342. */
  343. protected function writeCentralDirectoryHeader(OutputOffsetEntry $outEntry)
  344. {
  345. $entry = $outEntry->getEntry();
  346. $compressedSize = $entry->getCompressedSize();
  347. $size = $entry->getSize();
  348. // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
  349. // UNKNOWN!
  350. if (($compressedSize | $size) === ZipEntry::UNKNOWN) {
  351. throw new RuntimeException('invalid entry');
  352. }
  353. $extra = $entry->getExtra();
  354. $extraSize = \strlen($extra);
  355. $commentLength = \strlen($entry->getComment());
  356. fwrite(
  357. $this->out,
  358. pack(
  359. 'VvvvvVVVVvvvvvVV',
  360. // central file header signature 4 bytes (0x02014b50)
  361. self::CENTRAL_FILE_HEADER_SIG,
  362. // version made by 2 bytes
  363. ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
  364. // version needed to extract 2 bytes
  365. ($entry->getExtractedOS() << 8) | $entry->getVersionNeededToExtract(),
  366. // general purpose bit flag 2 bytes
  367. $entry->getGeneralPurposeBitFlags(),
  368. // compression method 2 bytes
  369. $entry->getMethod(),
  370. // last mod file datetime 4 bytes
  371. $entry->getDosTime(),
  372. // crc-32 4 bytes
  373. $entry->getCrc(),
  374. // compressed size 4 bytes
  375. $entry->getCompressedSize(),
  376. // uncompressed size 4 bytes
  377. $entry->getSize(),
  378. // file name length 2 bytes
  379. \strlen($entry->getName()),
  380. // extra field length 2 bytes
  381. $extraSize,
  382. // file comment length 2 bytes
  383. $commentLength,
  384. // disk number start 2 bytes
  385. 0,
  386. // internal file attributes 2 bytes
  387. $entry->getInternalAttributes(),
  388. // external file attributes 4 bytes
  389. $entry->getExternalAttributes(),
  390. // relative offset of local header 4 bytes
  391. $outEntry->getOffset()
  392. )
  393. );
  394. // file name (variable size)
  395. fwrite($this->out, $entry->getName());
  396. if ($extraSize > 0) {
  397. // extra field (variable size)
  398. fwrite($this->out, $extra);
  399. }
  400. if ($commentLength > 0) {
  401. // file comment (variable size)
  402. fwrite($this->out, $entry->getComment());
  403. }
  404. }
  405. /**
  406. * @param int $centralDirectoryOffset
  407. */
  408. protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset)
  409. {
  410. $cdEntriesCount = \count($this->zipModel);
  411. $position = ftell($this->out);
  412. $centralDirectorySize = $position - $centralDirectoryOffset;
  413. $cdEntriesZip64 = $cdEntriesCount > 0xFFFF;
  414. $cdSizeZip64 = $centralDirectorySize > 0xFFFFFFFF;
  415. $cdOffsetZip64 = $centralDirectoryOffset > 0xFFFFFFFF;
  416. $zip64Required = $cdEntriesZip64 || $cdSizeZip64 || $cdOffsetZip64;
  417. if ($zip64Required) {
  418. $zip64EndOfCentralDirectoryOffset = ftell($this->out);
  419. // find max software version, version needed to extract and most common platform
  420. list($softwareVersion, $versionNeededToExtract) = array_reduce(
  421. $this->zipModel->getEntries(),
  422. static function (array $carry, ZipEntry $entry) {
  423. $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
  424. $carry[1] = max($carry[1], $entry->getVersionNeededToExtract() & 0xFF);
  425. return $carry;
  426. },
  427. [10 /* simple file min ver */, 45 /* zip64 ext min ver */]
  428. );
  429. $createdOS = $extractedOS = ZipEntry::PLATFORM_FAT;
  430. $versionMadeBy = ($createdOS << 8) | max($softwareVersion, 45 /* zip64 ext min ver */);
  431. $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, 45 /* zip64 ext min ver */);
  432. // signature 4 bytes (0x06064b50)
  433. fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG));
  434. // size of zip64 end of central
  435. // directory record 8 bytes
  436. fwrite($this->out, PackUtil::packLongLE(44));
  437. fwrite(
  438. $this->out,
  439. pack(
  440. 'vvVV',
  441. // version made by 2 bytes
  442. $versionMadeBy & 0xFFFF,
  443. // version needed to extract 2 bytes
  444. $versionExtractedBy & 0xFFFF,
  445. // number of this disk 4 bytes
  446. 0,
  447. // number of the disk with the
  448. // start of the central directory 4 bytes
  449. 0
  450. )
  451. );
  452. // total number of entries in the
  453. // central directory on this disk 8 bytes
  454. fwrite($this->out, PackUtil::packLongLE($cdEntriesCount));
  455. // total number of entries in the
  456. // central directory 8 bytes
  457. fwrite($this->out, PackUtil::packLongLE($cdEntriesCount));
  458. // size of the central directory 8 bytes
  459. fwrite($this->out, PackUtil::packLongLE($centralDirectorySize));
  460. // offset of start of central
  461. // directory with respect to
  462. // the starting disk number 8 bytes
  463. fwrite($this->out, PackUtil::packLongLE($centralDirectoryOffset));
  464. // write zip64 end of central directory locator
  465. fwrite(
  466. $this->out,
  467. pack(
  468. 'VV',
  469. // zip64 end of central dir locator
  470. // signature 4 bytes (0x07064b50)
  471. EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_SIG,
  472. // number of the disk with the
  473. // start of the zip64 end of
  474. // central directory 4 bytes
  475. 0
  476. )
  477. );
  478. // relative offset of the zip64
  479. // end of central directory record 8 bytes
  480. fwrite($this->out, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
  481. // total number of disks 4 bytes
  482. fwrite($this->out, pack('V', 1));
  483. }
  484. $comment = $this->zipModel->getArchiveComment();
  485. $commentLength = $comment !== null ? \strlen($comment) : 0;
  486. fwrite(
  487. $this->out,
  488. pack(
  489. 'VvvvvVVv',
  490. // end of central dir signature 4 bytes (0x06054b50)
  491. EndOfCentralDirectory::END_OF_CD_SIG,
  492. // number of this disk 2 bytes
  493. 0,
  494. // number of the disk with the
  495. // start of the central directory 2 bytes
  496. 0,
  497. // total number of entries in the
  498. // central directory on this disk 2 bytes
  499. $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
  500. // total number of entries in
  501. // the central directory 2 bytes
  502. $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
  503. // size of the central directory 4 bytes
  504. $cdSizeZip64 ? 0xFFFFFFFF : $centralDirectorySize,
  505. // offset of start of central
  506. // directory with respect to
  507. // the starting disk number 4 bytes
  508. $cdOffsetZip64 ? 0xFFFFFFFF : $centralDirectoryOffset,
  509. // .ZIP file comment length 2 bytes
  510. $commentLength
  511. )
  512. );
  513. if ($commentLength > 0) {
  514. // .ZIP file comment (variable size)
  515. fwrite($this->out, $comment);
  516. }
  517. }
  518. /**
  519. * @return resource
  520. */
  521. public function getStream()
  522. {
  523. return $this->out;
  524. }
  525. }