ZipEntry.php 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the nelexa/zip package.
  5. * (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. namespace PhpZip\Model;
  10. use PhpZip\Constants\DosAttrs;
  11. use PhpZip\Constants\DosCodePage;
  12. use PhpZip\Constants\GeneralPurposeBitFlag;
  13. use PhpZip\Constants\UnixStat;
  14. use PhpZip\Constants\ZipCompressionLevel;
  15. use PhpZip\Constants\ZipCompressionMethod;
  16. use PhpZip\Constants\ZipConstants;
  17. use PhpZip\Constants\ZipEncryptionMethod;
  18. use PhpZip\Constants\ZipPlatform;
  19. use PhpZip\Constants\ZipVersion;
  20. use PhpZip\Exception\InvalidArgumentException;
  21. use PhpZip\Exception\RuntimeException;
  22. use PhpZip\Exception\ZipUnsupportMethodException;
  23. use PhpZip\Model\Extra\ExtraFieldsCollection;
  24. use PhpZip\Model\Extra\Fields\AsiExtraField;
  25. use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField;
  26. use PhpZip\Model\Extra\Fields\NtfsExtraField;
  27. use PhpZip\Model\Extra\Fields\OldUnixExtraField;
  28. use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
  29. use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
  30. use PhpZip\Model\Extra\ZipExtraField;
  31. use PhpZip\Util\DateTimeConverter;
  32. use PhpZip\Util\StringUtil;
  33. /**
  34. * ZIP file entry.
  35. *
  36. * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  37. */
  38. class ZipEntry
  39. {
  40. /** @var int the unknown value for numeric properties */
  41. public const UNKNOWN = -1;
  42. /** @var string Entry name (filename in archive) */
  43. private string $name;
  44. /** @var bool Is directory */
  45. private bool $isDirectory;
  46. /** @var ZipData|null Zip entry contents */
  47. private ?ZipData $data = null;
  48. /** @var int Made by platform */
  49. private int $createdOS = self::UNKNOWN;
  50. /** @var int Extracted by platform */
  51. private int $extractedOS = self::UNKNOWN;
  52. /** @var int Software version */
  53. private int $softwareVersion = self::UNKNOWN;
  54. /** @var int Version needed to extract */
  55. private int $extractVersion = self::UNKNOWN;
  56. /** @var int Compression method */
  57. private int $compressionMethod = self::UNKNOWN;
  58. /** @var int General purpose bit flags */
  59. private int $generalPurposeBitFlags = 0;
  60. /** @var int Dos time */
  61. private int $dosTime = self::UNKNOWN;
  62. /** @var int Crc32 */
  63. private int $crc = self::UNKNOWN;
  64. /** @var int Compressed size */
  65. private int $compressedSize = self::UNKNOWN;
  66. /** @var int Uncompressed size */
  67. private int $uncompressedSize = self::UNKNOWN;
  68. /** @var int Internal attributes */
  69. private int $internalAttributes = 0;
  70. /** @var int External attributes */
  71. private int $externalAttributes = 0;
  72. /** @var int relative Offset Of Local File Header */
  73. private int $localHeaderOffset = 0;
  74. /**
  75. * Collections of Extra Fields in Central Directory.
  76. * Keys from Header ID [int] and value Extra Field [ExtraField].
  77. */
  78. protected ExtraFieldsCollection $cdExtraFields;
  79. /**
  80. * Collections of Extra Fields int local header.
  81. * Keys from Header ID [int] and value Extra Field [ExtraField].
  82. */
  83. protected ExtraFieldsCollection $localExtraFields;
  84. /** @var string|null comment field */
  85. private ?string $comment = null;
  86. /** @var string|null entry password for read or write encryption data */
  87. private ?string $password = null;
  88. /** @var int encryption method */
  89. private int $encryptionMethod = ZipEncryptionMethod::NONE;
  90. /** @var int compression level */
  91. private int $compressionLevel = ZipCompressionLevel::NORMAL;
  92. /** @var string|null entry name charset */
  93. private ?string $charset = null;
  94. /**
  95. * @param string $name Entry name
  96. * @param string|null $charset Entry name charset
  97. */
  98. public function __construct(string $name, ?string $charset = null)
  99. {
  100. $this->setName($name, $charset);
  101. $this->cdExtraFields = new ExtraFieldsCollection();
  102. $this->localExtraFields = new ExtraFieldsCollection();
  103. }
  104. /**
  105. * This method only internal use.
  106. *
  107. * @internal
  108. *
  109. * @noinspection PhpTooManyParametersInspection
  110. *
  111. * @param ?string $comment
  112. * @param ?string $charset
  113. *
  114. * @return ZipEntry
  115. */
  116. public static function create(
  117. string $name,
  118. int $createdOS,
  119. int $extractedOS,
  120. int $softwareVersion,
  121. int $extractVersion,
  122. int $compressionMethod,
  123. int $gpbf,
  124. int $dosTime,
  125. int $crc,
  126. int $compressedSize,
  127. int $uncompressedSize,
  128. int $internalAttributes,
  129. int $externalAttributes,
  130. int $offsetLocalHeader,
  131. ?string $comment = null,
  132. ?string $charset = null
  133. ): self {
  134. $entry = new self($name);
  135. $entry->createdOS = $createdOS;
  136. $entry->extractedOS = $extractedOS;
  137. $entry->softwareVersion = $softwareVersion;
  138. $entry->extractVersion = $extractVersion;
  139. $entry->compressionMethod = $compressionMethod;
  140. $entry->generalPurposeBitFlags = $gpbf;
  141. $entry->dosTime = $dosTime;
  142. $entry->crc = $crc;
  143. $entry->compressedSize = $compressedSize;
  144. $entry->uncompressedSize = $uncompressedSize;
  145. $entry->internalAttributes = $internalAttributes;
  146. $entry->externalAttributes = $externalAttributes;
  147. $entry->localHeaderOffset = $offsetLocalHeader;
  148. $entry->setComment($comment);
  149. $entry->setCharset($charset);
  150. $entry->updateCompressionLevel();
  151. return $entry;
  152. }
  153. /**
  154. * Set entry name.
  155. *
  156. * @param string $name New entry name
  157. * @param string|null $charset Entry name charset
  158. *
  159. * @return ZipEntry
  160. */
  161. private function setName(string $name, ?string $charset = null): self
  162. {
  163. $name = ltrim($name, '\\/');
  164. if ($name === '') {
  165. throw new InvalidArgumentException('Empty zip entry name');
  166. }
  167. $length = \strlen($name);
  168. if ($length > 0xFFFF) {
  169. throw new InvalidArgumentException('Illegal zip entry name parameter');
  170. }
  171. $this->setCharset($charset);
  172. if ($this->charset === null && !StringUtil::isASCII($name)) {
  173. $this->enableUtf8Name(true);
  174. }
  175. $this->name = $name;
  176. $this->isDirectory = ($length = \strlen($name)) >= 1 && $name[$length - 1] === '/';
  177. $this->externalAttributes = $this->isDirectory ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE;
  178. if ($this->extractVersion !== self::UNKNOWN) {
  179. $this->extractVersion = max(
  180. $this->extractVersion,
  181. $this->isDirectory
  182. ? ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO
  183. : ZipVersion::v10_DEFAULT_MIN
  184. );
  185. }
  186. return $this;
  187. }
  188. /**
  189. * @see DosCodePage::getCodePages()
  190. *
  191. * @param ?string $charset
  192. *
  193. * @return ZipEntry
  194. */
  195. public function setCharset(?string $charset = null): self
  196. {
  197. if ($charset !== null && $charset === '') {
  198. throw new InvalidArgumentException('Empty charset');
  199. }
  200. $this->charset = $charset;
  201. return $this;
  202. }
  203. public function getCharset(): ?string
  204. {
  205. return $this->charset;
  206. }
  207. /**
  208. * @param string $newName New entry name
  209. *
  210. * @return ZipEntry new {@see ZipEntry} object with new name
  211. *
  212. * @internal
  213. */
  214. public function rename(string $newName): self
  215. {
  216. $newEntry = clone $this;
  217. $newEntry->setName($newName);
  218. $newEntry->removeExtraField(UnicodePathExtraField::HEADER_ID);
  219. return $newEntry;
  220. }
  221. /**
  222. * Returns the ZIP entry name.
  223. */
  224. public function getName(): string
  225. {
  226. return $this->name;
  227. }
  228. public function getData(): ?ZipData
  229. {
  230. return $this->data;
  231. }
  232. public function setData(?ZipData $data): void
  233. {
  234. $this->data = $data;
  235. }
  236. /**
  237. * @return int platform
  238. */
  239. public function getCreatedOS(): int
  240. {
  241. return $this->createdOS;
  242. }
  243. /**
  244. * Set platform.
  245. *
  246. * @return ZipEntry
  247. */
  248. public function setCreatedOS(int $platform): self
  249. {
  250. if ($platform < 0x00 || $platform > 0xFF) {
  251. throw new InvalidArgumentException('Platform out of range');
  252. }
  253. $this->createdOS = $platform;
  254. return $this;
  255. }
  256. public function getExtractedOS(): int
  257. {
  258. return $this->extractedOS;
  259. }
  260. /**
  261. * Set extracted OS.
  262. *
  263. * @return ZipEntry
  264. */
  265. public function setExtractedOS(int $platform): self
  266. {
  267. if ($platform < 0x00 || $platform > 0xFF) {
  268. throw new InvalidArgumentException('Platform out of range');
  269. }
  270. $this->extractedOS = $platform;
  271. return $this;
  272. }
  273. public function getSoftwareVersion(): int
  274. {
  275. if ($this->softwareVersion === self::UNKNOWN) {
  276. return $this->getExtractVersion();
  277. }
  278. return $this->softwareVersion;
  279. }
  280. /**
  281. * @return ZipEntry
  282. */
  283. public function setSoftwareVersion(int $softwareVersion): self
  284. {
  285. $this->softwareVersion = $softwareVersion;
  286. return $this;
  287. }
  288. /**
  289. * Version needed to extract.
  290. */
  291. public function getExtractVersion(): int
  292. {
  293. if ($this->extractVersion === self::UNKNOWN) {
  294. if (ZipEncryptionMethod::isWinZipAesMethod($this->encryptionMethod)) {
  295. return ZipVersion::v51_ENCR_AES_RC2_CORRECT;
  296. }
  297. if ($this->compressionMethod === ZipCompressionMethod::BZIP2) {
  298. return ZipVersion::v46_BZIP2;
  299. }
  300. if ($this->isZip64ExtensionsRequired()) {
  301. return ZipVersion::v45_ZIP64_EXT;
  302. }
  303. if (
  304. $this->compressionMethod === ZipCompressionMethod::DEFLATED
  305. || $this->isDirectory
  306. || $this->encryptionMethod === ZipEncryptionMethod::PKWARE
  307. ) {
  308. return ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO;
  309. }
  310. return ZipVersion::v10_DEFAULT_MIN;
  311. }
  312. return $this->extractVersion;
  313. }
  314. /**
  315. * Set version needed to extract.
  316. *
  317. * @return ZipEntry
  318. */
  319. public function setExtractVersion(int $version): self
  320. {
  321. $this->extractVersion = max(ZipVersion::v10_DEFAULT_MIN, $version);
  322. return $this;
  323. }
  324. /**
  325. * Returns the compressed size of this entry.
  326. */
  327. public function getCompressedSize(): int
  328. {
  329. return $this->compressedSize;
  330. }
  331. /**
  332. * Sets the compressed size of this entry.
  333. *
  334. * @param int $compressedSize the Compressed Size
  335. *
  336. * @return ZipEntry
  337. *
  338. * @internal
  339. */
  340. public function setCompressedSize(int $compressedSize): self
  341. {
  342. if ($compressedSize < self::UNKNOWN) {
  343. throw new InvalidArgumentException('Compressed size < ' . self::UNKNOWN);
  344. }
  345. $this->compressedSize = $compressedSize;
  346. return $this;
  347. }
  348. /**
  349. * Returns the uncompressed size of this entry.
  350. */
  351. public function getUncompressedSize(): int
  352. {
  353. return $this->uncompressedSize;
  354. }
  355. /**
  356. * Sets the uncompressed size of this entry.
  357. *
  358. * @param int $uncompressedSize the (Uncompressed) Size
  359. *
  360. * @return ZipEntry
  361. *
  362. * @internal
  363. */
  364. public function setUncompressedSize(int $uncompressedSize): self
  365. {
  366. if ($uncompressedSize < self::UNKNOWN) {
  367. throw new InvalidArgumentException('Uncompressed size < ' . self::UNKNOWN);
  368. }
  369. $this->uncompressedSize = $uncompressedSize;
  370. return $this;
  371. }
  372. /**
  373. * Return relative Offset Of Local File Header.
  374. */
  375. public function getLocalHeaderOffset(): int
  376. {
  377. return $this->localHeaderOffset;
  378. }
  379. /**
  380. * @return ZipEntry
  381. *
  382. * @internal
  383. */
  384. public function setLocalHeaderOffset(int $localHeaderOffset): self
  385. {
  386. if ($localHeaderOffset < 0) {
  387. throw new InvalidArgumentException('Negative $localHeaderOffset');
  388. }
  389. $this->localHeaderOffset = $localHeaderOffset;
  390. return $this;
  391. }
  392. /**
  393. * Returns the General Purpose Bit Flags.
  394. */
  395. public function getGeneralPurposeBitFlags(): int
  396. {
  397. return $this->generalPurposeBitFlags;
  398. }
  399. /**
  400. * Sets the General Purpose Bit Flags.
  401. *
  402. * @param int $gpbf general purpose bit flags
  403. *
  404. * @return ZipEntry
  405. *
  406. * @internal
  407. */
  408. public function setGeneralPurposeBitFlags(int $gpbf): self
  409. {
  410. if ($gpbf < 0x0000 || $gpbf > 0xFFFF) {
  411. throw new InvalidArgumentException('general purpose bit flags out of range');
  412. }
  413. $this->generalPurposeBitFlags = $gpbf;
  414. $this->updateCompressionLevel();
  415. return $this;
  416. }
  417. private function updateCompressionLevel(): void
  418. {
  419. if ($this->compressionMethod === ZipCompressionMethod::DEFLATED) {
  420. $bit1 = $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::COMPRESSION_FLAG1);
  421. $bit2 = $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::COMPRESSION_FLAG2);
  422. if ($bit1 && !$bit2) {
  423. $this->compressionLevel = ZipCompressionLevel::MAXIMUM;
  424. } elseif (!$bit1 && $bit2) {
  425. $this->compressionLevel = ZipCompressionLevel::FAST;
  426. } elseif ($bit1 && $bit2) {
  427. $this->compressionLevel = ZipCompressionLevel::SUPER_FAST;
  428. } else {
  429. $this->compressionLevel = ZipCompressionLevel::NORMAL;
  430. }
  431. }
  432. }
  433. /**
  434. * @return ZipEntry
  435. */
  436. private function setGeneralBitFlag(int $mask, bool $enable): self
  437. {
  438. if ($enable) {
  439. $this->generalPurposeBitFlags |= $mask;
  440. } else {
  441. $this->generalPurposeBitFlags &= ~$mask;
  442. }
  443. return $this;
  444. }
  445. private function isSetGeneralBitFlag(int $mask): bool
  446. {
  447. return ($this->generalPurposeBitFlags & $mask) === $mask;
  448. }
  449. public function isDataDescriptorEnabled(): bool
  450. {
  451. return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::DATA_DESCRIPTOR);
  452. }
  453. /**
  454. * Enabling or disabling the use of the Data Descriptor block.
  455. */
  456. public function enableDataDescriptor(bool $enabled = true): void
  457. {
  458. $this->setGeneralBitFlag(GeneralPurposeBitFlag::DATA_DESCRIPTOR, $enabled);
  459. }
  460. public function enableUtf8Name(bool $enabled): void
  461. {
  462. $this->setGeneralBitFlag(GeneralPurposeBitFlag::UTF8, $enabled);
  463. }
  464. public function isUtf8Flag(): bool
  465. {
  466. return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::UTF8);
  467. }
  468. /**
  469. * Returns true if and only if this ZIP entry is encrypted.
  470. */
  471. public function isEncrypted(): bool
  472. {
  473. return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::ENCRYPTION);
  474. }
  475. public function isStrongEncryption(): bool
  476. {
  477. return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::STRONG_ENCRYPTION);
  478. }
  479. /**
  480. * Sets the encryption property to false and removes any other
  481. * encryption artifacts.
  482. *
  483. * @return ZipEntry
  484. */
  485. public function disableEncryption(): self
  486. {
  487. $this->setEncrypted(false);
  488. $this->removeExtraField(WinZipAesExtraField::HEADER_ID);
  489. $this->encryptionMethod = ZipEncryptionMethod::NONE;
  490. $this->password = null;
  491. $this->extractVersion = self::UNKNOWN;
  492. return $this;
  493. }
  494. /**
  495. * Sets the encryption flag for this ZIP entry.
  496. *
  497. * @return ZipEntry
  498. */
  499. private function setEncrypted(bool $encrypted): self
  500. {
  501. $this->setGeneralBitFlag(GeneralPurposeBitFlag::ENCRYPTION, $encrypted);
  502. return $this;
  503. }
  504. /**
  505. * Returns the compression method for this entry.
  506. */
  507. public function getCompressionMethod(): int
  508. {
  509. return $this->compressionMethod;
  510. }
  511. /**
  512. * Sets the compression method for this entry.
  513. *
  514. * @throws ZipUnsupportMethodException
  515. *
  516. * @return ZipEntry
  517. *
  518. * @see ZipCompressionMethod::STORED
  519. * @see ZipCompressionMethod::DEFLATED
  520. * @see ZipCompressionMethod::BZIP2
  521. */
  522. public function setCompressionMethod(int $compressionMethod): self
  523. {
  524. if ($compressionMethod < 0x0000 || $compressionMethod > 0xFFFF) {
  525. throw new InvalidArgumentException('method out of range: ' . $compressionMethod);
  526. }
  527. ZipCompressionMethod::checkSupport($compressionMethod);
  528. $this->compressionMethod = $compressionMethod;
  529. $this->updateCompressionLevel();
  530. $this->extractVersion = self::UNKNOWN;
  531. return $this;
  532. }
  533. /**
  534. * Get Unix Timestamp.
  535. */
  536. public function getTime(): int
  537. {
  538. if ($this->getDosTime() === self::UNKNOWN) {
  539. return self::UNKNOWN;
  540. }
  541. return DateTimeConverter::msDosToUnix($this->getDosTime());
  542. }
  543. /**
  544. * Get Dos Time.
  545. */
  546. public function getDosTime(): int
  547. {
  548. return $this->dosTime;
  549. }
  550. /**
  551. * Set Dos Time.
  552. *
  553. * @return ZipEntry
  554. */
  555. public function setDosTime(int $dosTime): self
  556. {
  557. if (\PHP_INT_SIZE === 8) {
  558. if ($dosTime < 0x00000000 || $dosTime > 0xFFFFFFFF) {
  559. throw new InvalidArgumentException('DosTime out of range');
  560. }
  561. }
  562. $this->dosTime = $dosTime;
  563. return $this;
  564. }
  565. /**
  566. * Set time from unix timestamp.
  567. *
  568. * @return ZipEntry
  569. */
  570. public function setTime(int $unixTimestamp): self
  571. {
  572. if ($unixTimestamp !== self::UNKNOWN) {
  573. $this->setDosTime(DateTimeConverter::unixToMsDos($unixTimestamp));
  574. } else {
  575. $this->dosTime = 0;
  576. }
  577. return $this;
  578. }
  579. /**
  580. * Returns the external file attributes.
  581. *
  582. * @return int the external file attributes
  583. */
  584. public function getExternalAttributes(): int
  585. {
  586. return $this->externalAttributes;
  587. }
  588. /**
  589. * Sets the external file attributes.
  590. *
  591. * @param int $externalAttributes the external file attributes
  592. *
  593. * @return ZipEntry
  594. */
  595. public function setExternalAttributes(int $externalAttributes): self
  596. {
  597. $this->externalAttributes = $externalAttributes;
  598. if (\PHP_INT_SIZE === 8) {
  599. if ($externalAttributes < 0x00000000 || $externalAttributes > 0xFFFFFFFF) {
  600. throw new InvalidArgumentException('external attributes out of range: ' . $externalAttributes);
  601. }
  602. }
  603. $this->externalAttributes = $externalAttributes;
  604. return $this;
  605. }
  606. /**
  607. * Returns the internal file attributes.
  608. *
  609. * @return int the internal file attributes
  610. */
  611. public function getInternalAttributes(): int
  612. {
  613. return $this->internalAttributes;
  614. }
  615. /**
  616. * Sets the internal file attributes.
  617. *
  618. * @param int $internalAttributes the internal file attributes
  619. *
  620. * @return ZipEntry
  621. */
  622. public function setInternalAttributes(int $internalAttributes): self
  623. {
  624. if ($internalAttributes < 0x0000 || $internalAttributes > 0xFFFF) {
  625. throw new InvalidArgumentException('internal attributes out of range');
  626. }
  627. $this->internalAttributes = $internalAttributes;
  628. return $this;
  629. }
  630. /**
  631. * Returns true if and only if this ZIP entry represents a directory entry
  632. * (i.e. end with '/').
  633. */
  634. final public function isDirectory(): bool
  635. {
  636. return $this->isDirectory;
  637. }
  638. public function getCdExtraFields(): ExtraFieldsCollection
  639. {
  640. return $this->cdExtraFields;
  641. }
  642. public function getCdExtraField(int $headerId): ?ZipExtraField
  643. {
  644. return $this->cdExtraFields->get($headerId);
  645. }
  646. /**
  647. * @return ZipEntry
  648. */
  649. public function setCdExtraFields(ExtraFieldsCollection $cdExtraFields): self
  650. {
  651. $this->cdExtraFields = $cdExtraFields;
  652. return $this;
  653. }
  654. public function getLocalExtraFields(): ExtraFieldsCollection
  655. {
  656. return $this->localExtraFields;
  657. }
  658. public function getLocalExtraField(int $headerId): ?ZipExtraField
  659. {
  660. return $this->localExtraFields[$headerId];
  661. }
  662. /**
  663. * @return ZipEntry
  664. */
  665. public function setLocalExtraFields(ExtraFieldsCollection $localExtraFields): self
  666. {
  667. $this->localExtraFields = $localExtraFields;
  668. return $this;
  669. }
  670. public function getExtraField(int $headerId): ?ZipExtraField
  671. {
  672. $local = $this->getLocalExtraField($headerId);
  673. return $local ?? $this->getCdExtraField($headerId);
  674. }
  675. public function hasExtraField(int $headerId): bool
  676. {
  677. return isset($this->localExtraFields[$headerId])
  678. || isset($this->cdExtraFields[$headerId]);
  679. }
  680. public function removeExtraField(int $headerId): void
  681. {
  682. $this->cdExtraFields->remove($headerId);
  683. $this->localExtraFields->remove($headerId);
  684. }
  685. public function addExtraField(ZipExtraField $zipExtraField): void
  686. {
  687. $this->addLocalExtraField($zipExtraField);
  688. $this->addCdExtraField($zipExtraField);
  689. }
  690. public function addLocalExtraField(ZipExtraField $zipExtraField): void
  691. {
  692. $this->localExtraFields->add($zipExtraField);
  693. }
  694. public function addCdExtraField(ZipExtraField $zipExtraField): void
  695. {
  696. $this->cdExtraFields->add($zipExtraField);
  697. }
  698. /**
  699. * Returns comment entry.
  700. */
  701. public function getComment(): string
  702. {
  703. return $this->comment ?? '';
  704. }
  705. /**
  706. * Set entry comment.
  707. *
  708. * @param ?string $comment
  709. *
  710. * @return ZipEntry
  711. */
  712. public function setComment(?string $comment): self
  713. {
  714. if ($comment !== null) {
  715. $commentLength = \strlen($comment);
  716. if ($commentLength > 0xFFFF) {
  717. throw new InvalidArgumentException('Comment too long');
  718. }
  719. if ($this->charset === null && !StringUtil::isASCII($comment)) {
  720. $this->enableUtf8Name(true);
  721. }
  722. }
  723. $this->comment = $comment;
  724. return $this;
  725. }
  726. public function isDataDescriptorRequired(): bool
  727. {
  728. return ($this->getCrc() | $this->getCompressedSize() | $this->getUncompressedSize()) === self::UNKNOWN;
  729. }
  730. /**
  731. * Return crc32 content or 0 for WinZip AES v2.
  732. */
  733. public function getCrc(): int
  734. {
  735. return $this->crc;
  736. }
  737. /**
  738. * Set crc32 content.
  739. *
  740. * @return ZipEntry
  741. *
  742. * @internal
  743. */
  744. public function setCrc(int $crc): self
  745. {
  746. $this->crc = $crc;
  747. return $this;
  748. }
  749. public function getPassword(): ?string
  750. {
  751. return $this->password;
  752. }
  753. /**
  754. * Set password and encryption method from entry.
  755. *
  756. * @param ?string $password
  757. * @param ?int $encryptionMethod
  758. *
  759. * @return ZipEntry
  760. */
  761. public function setPassword(?string $password, ?int $encryptionMethod = null): self
  762. {
  763. if (!$this->isDirectory) {
  764. if ($password === null || $password === '') {
  765. $this->password = null;
  766. $this->disableEncryption();
  767. } else {
  768. $this->password = $password;
  769. if ($encryptionMethod === null && $this->encryptionMethod === ZipEncryptionMethod::NONE) {
  770. $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256;
  771. }
  772. if ($encryptionMethod !== null) {
  773. $this->setEncryptionMethod($encryptionMethod);
  774. }
  775. $this->setEncrypted(true);
  776. }
  777. }
  778. return $this;
  779. }
  780. public function getEncryptionMethod(): int
  781. {
  782. return $this->encryptionMethod;
  783. }
  784. /**
  785. * Set encryption method.
  786. *
  787. * @see ZipEncryptionMethod::NONE
  788. * @see ZipEncryptionMethod::PKWARE
  789. * @see ZipEncryptionMethod::WINZIP_AES_256
  790. * @see ZipEncryptionMethod::WINZIP_AES_192
  791. * @see ZipEncryptionMethod::WINZIP_AES_128
  792. *
  793. * @param ?int $encryptionMethod
  794. *
  795. * @return ZipEntry
  796. */
  797. public function setEncryptionMethod(?int $encryptionMethod): self
  798. {
  799. $method = $encryptionMethod ?? ZipEncryptionMethod::NONE;
  800. ZipEncryptionMethod::checkSupport($method);
  801. $this->encryptionMethod = $method;
  802. $this->setEncrypted($this->encryptionMethod !== ZipEncryptionMethod::NONE);
  803. $this->extractVersion = self::UNKNOWN;
  804. return $this;
  805. }
  806. public function getCompressionLevel(): int
  807. {
  808. return $this->compressionLevel;
  809. }
  810. /**
  811. * @return ZipEntry
  812. */
  813. public function setCompressionLevel(int $compressionLevel): self
  814. {
  815. if ($compressionLevel === self::UNKNOWN) {
  816. $compressionLevel = ZipCompressionLevel::NORMAL;
  817. }
  818. if (
  819. $compressionLevel < ZipCompressionLevel::LEVEL_MIN
  820. || $compressionLevel > ZipCompressionLevel::LEVEL_MAX
  821. ) {
  822. throw new InvalidArgumentException(
  823. 'Invalid compression level. Minimum level '
  824. . ZipCompressionLevel::LEVEL_MIN . '. Maximum level ' . ZipCompressionLevel::LEVEL_MAX
  825. );
  826. }
  827. $this->compressionLevel = $compressionLevel;
  828. $this->updateGbpfCompLevel();
  829. return $this;
  830. }
  831. /**
  832. * Update general purpose bit flogs.
  833. */
  834. private function updateGbpfCompLevel(): void
  835. {
  836. if ($this->compressionMethod === ZipCompressionMethod::DEFLATED) {
  837. $bit1 = false;
  838. $bit2 = false;
  839. switch ($this->compressionLevel) {
  840. case ZipCompressionLevel::MAXIMUM:
  841. $bit1 = true;
  842. break;
  843. case ZipCompressionLevel::FAST:
  844. $bit2 = true;
  845. break;
  846. case ZipCompressionLevel::SUPER_FAST:
  847. $bit1 = true;
  848. $bit2 = true;
  849. break;
  850. // default is ZipCompressionLevel::NORMAL
  851. }
  852. $this->generalPurposeBitFlags |= ($bit1 ? GeneralPurposeBitFlag::COMPRESSION_FLAG1 : 0);
  853. $this->generalPurposeBitFlags |= ($bit2 ? GeneralPurposeBitFlag::COMPRESSION_FLAG2 : 0);
  854. }
  855. }
  856. /**
  857. * Sets Unix permissions in a way that is understood by Info-Zip's
  858. * unzip command.
  859. *
  860. * @param int $mode mode an int value
  861. *
  862. * @return ZipEntry
  863. */
  864. public function setUnixMode(int $mode): self
  865. {
  866. $this->setExternalAttributes(
  867. ($mode << 16)
  868. // MS-DOS read-only attribute
  869. | (($mode & UnixStat::UNX_IWUSR) === 0 ? DosAttrs::DOS_HIDDEN : 0)
  870. // MS-DOS directory flag
  871. | ($this->isDirectory() ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE)
  872. );
  873. $this->createdOS = ZipPlatform::OS_UNIX;
  874. return $this;
  875. }
  876. /**
  877. * Unix permission.
  878. *
  879. * @return int the unix permissions
  880. */
  881. public function getUnixMode(): int
  882. {
  883. $mode = 0;
  884. if ($this->createdOS === ZipPlatform::OS_UNIX) {
  885. $mode = ($this->externalAttributes >> 16) & 0xFFFF;
  886. } elseif ($this->hasExtraField(AsiExtraField::HEADER_ID)) {
  887. /** @var AsiExtraField $asiExtraField */
  888. $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID);
  889. $mode = $asiExtraField->getMode();
  890. }
  891. if ($mode > 0) {
  892. return $mode;
  893. }
  894. return $this->isDirectory ? 040755 : 0100644;
  895. }
  896. /**
  897. * Offset MUST be considered in decision about ZIP64 format - see
  898. * description of Data Descriptor in ZIP File Format Specification.
  899. */
  900. public function isZip64ExtensionsRequired(): bool
  901. {
  902. return $this->compressedSize > ZipConstants::ZIP64_MAGIC
  903. || $this->uncompressedSize > ZipConstants::ZIP64_MAGIC;
  904. }
  905. /**
  906. * Returns true if this entry represents a unix symlink,
  907. * in which case the entry's content contains the target path
  908. * for the symlink.
  909. *
  910. * @return bool true if the entry represents a unix symlink,
  911. * false otherwise
  912. */
  913. public function isUnixSymlink(): bool
  914. {
  915. return ($this->getUnixMode() & UnixStat::UNX_IFMT) === UnixStat::UNX_IFLNK;
  916. }
  917. public function getMTime(): \DateTimeInterface
  918. {
  919. /** @var NtfsExtraField|null $ntfsExtra */
  920. $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID);
  921. if ($ntfsExtra !== null) {
  922. return $ntfsExtra->getModifyDateTime();
  923. }
  924. /** @var ExtendedTimestampExtraField|null $extendedExtra */
  925. $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID);
  926. if ($extendedExtra !== null && ($mtime = $extendedExtra->getModifyDateTime()) !== null) {
  927. return $mtime;
  928. }
  929. /** @var OldUnixExtraField|null $oldUnixExtra */
  930. $oldUnixExtra = $this->getExtraField(OldUnixExtraField::HEADER_ID);
  931. if ($oldUnixExtra !== null && ($mtime = $oldUnixExtra->getModifyDateTime()) !== null) {
  932. return $mtime;
  933. }
  934. $timestamp = $this->getTime();
  935. try {
  936. return new \DateTimeImmutable('@' . $timestamp);
  937. } catch (\Exception $e) {
  938. throw new RuntimeException('Error create DateTime object with timestamp ' . $timestamp, 1, $e);
  939. }
  940. }
  941. public function getATime(): ?\DateTimeInterface
  942. {
  943. /** @var NtfsExtraField|null $ntfsExtra */
  944. $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID);
  945. if ($ntfsExtra !== null) {
  946. return $ntfsExtra->getAccessDateTime();
  947. }
  948. /** @var ExtendedTimestampExtraField|null $extendedExtra */
  949. $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID);
  950. if ($extendedExtra !== null && ($atime = $extendedExtra->getAccessDateTime()) !== null) {
  951. return $atime;
  952. }
  953. /** @var OldUnixExtraField|null $oldUnixExtra */
  954. $oldUnixExtra = $this->getExtraField(OldUnixExtraField::HEADER_ID);
  955. if ($oldUnixExtra !== null) {
  956. return $oldUnixExtra->getAccessDateTime();
  957. }
  958. return null;
  959. }
  960. public function getCTime(): ?\DateTimeInterface
  961. {
  962. /** @var NtfsExtraField|null $ntfsExtra */
  963. $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID);
  964. if ($ntfsExtra !== null) {
  965. return $ntfsExtra->getCreateDateTime();
  966. }
  967. /** @var ExtendedTimestampExtraField|null $extendedExtra */
  968. $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID);
  969. if ($extendedExtra !== null) {
  970. return $extendedExtra->getCreateDateTime();
  971. }
  972. return null;
  973. }
  974. public function __clone()
  975. {
  976. $this->cdExtraFields = clone $this->cdExtraFields;
  977. $this->localExtraFields = clone $this->localExtraFields;
  978. if ($this->data !== null) {
  979. $this->data = clone $this->data;
  980. }
  981. }
  982. }