ZipAbstractEntry.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. <?php
  2. namespace PhpZip\Model\Entry;
  3. use PhpZip\Exception\InvalidArgumentException;
  4. use PhpZip\Exception\ZipException;
  5. use PhpZip\Extra\ExtraFieldsCollection;
  6. use PhpZip\Extra\ExtraFieldsFactory;
  7. use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
  8. use PhpZip\Model\ZipEntry;
  9. use PhpZip\Util\DateTimeConverter;
  10. use PhpZip\Util\StringUtil;
  11. use PhpZip\ZipFileInterface;
  12. /**
  13. * Abstract ZIP entry.
  14. *
  15. * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  16. *
  17. * @author Ne-Lexa alexey@nelexa.ru
  18. * @license MIT
  19. */
  20. abstract class ZipAbstractEntry implements ZipEntry
  21. {
  22. /** @var int bit flags for init state */
  23. private $init;
  24. /** @var string Entry name (filename in archive) */
  25. private $name;
  26. /** @var int Made by platform */
  27. private $platform;
  28. /** @var int */
  29. private $versionNeededToExtract = 20;
  30. /** @var int Compression method */
  31. private $method;
  32. /** @var int */
  33. private $general;
  34. /** @var int Dos time */
  35. private $dosTime;
  36. /** @var int Crc32 */
  37. private $crc;
  38. /** @var int Compressed size */
  39. private $compressedSize = self::UNKNOWN;
  40. /** @var int Uncompressed size */
  41. private $size = self::UNKNOWN;
  42. /** @var int External attributes */
  43. private $externalAttributes;
  44. /** @var int relative Offset Of Local File Header */
  45. private $offset = self::UNKNOWN;
  46. /**
  47. * Collections of Extra Fields.
  48. * Keys from Header ID [int] and value Extra Field [ExtraField].
  49. * Should be null or may be empty if no Extra Fields are used.
  50. *
  51. * @var ExtraFieldsCollection
  52. */
  53. private $extraFieldsCollection;
  54. /** @var string comment field */
  55. private $comment;
  56. /** @var string entry password for read or write encryption data */
  57. private $password;
  58. /**
  59. * Encryption method.
  60. *
  61. * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
  62. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
  63. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
  64. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
  65. *
  66. * @var int
  67. */
  68. private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL;
  69. /** @var int */
  70. private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
  71. /**
  72. * ZipAbstractEntry constructor.
  73. */
  74. public function __construct()
  75. {
  76. $this->extraFieldsCollection = new ExtraFieldsCollection();
  77. }
  78. /**
  79. * @param ZipEntry $entry
  80. *
  81. * @throws ZipException
  82. */
  83. public function setEntry(ZipEntry $entry)
  84. {
  85. $this->setName($entry->getName());
  86. $this->setPlatform($entry->getPlatform());
  87. $this->setVersionNeededToExtract($entry->getVersionNeededToExtract());
  88. $this->setMethod($entry->getMethod());
  89. $this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags());
  90. $this->setDosTime($entry->getDosTime());
  91. $this->setCrc($entry->getCrc());
  92. $this->setCompressedSize($entry->getCompressedSize());
  93. $this->setSize($entry->getSize());
  94. $this->setExternalAttributes($entry->getExternalAttributes());
  95. $this->setOffset($entry->getOffset());
  96. $this->setExtra($entry->getExtra());
  97. $this->setComment($entry->getComment());
  98. $this->setPassword($entry->getPassword());
  99. $this->setEncryptionMethod($entry->getEncryptionMethod());
  100. $this->setCompressionLevel($entry->getCompressionLevel());
  101. $this->setEncrypted($entry->isEncrypted());
  102. }
  103. /**
  104. * Returns the ZIP entry name.
  105. *
  106. * @return string
  107. */
  108. public function getName()
  109. {
  110. return $this->name;
  111. }
  112. /**
  113. * Set entry name.
  114. *
  115. * @param string $name New entry name
  116. *
  117. * @throws ZipException
  118. *
  119. * @return ZipEntry
  120. */
  121. public function setName($name)
  122. {
  123. $length = \strlen($name);
  124. if ($length < 0x0000 || $length > 0xffff) {
  125. throw new ZipException('Illegal zip entry name parameter');
  126. }
  127. $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true);
  128. $this->name = $name;
  129. return $this;
  130. }
  131. /**
  132. * Sets the indexed General Purpose Bit Flag.
  133. *
  134. * @param int $mask
  135. * @param bool $bit
  136. *
  137. * @return ZipEntry
  138. */
  139. public function setGeneralPurposeBitFlag($mask, $bit)
  140. {
  141. if ($bit) {
  142. $this->general |= $mask;
  143. } else {
  144. $this->general &= ~$mask;
  145. }
  146. return $this;
  147. }
  148. /**
  149. * @return int Get platform
  150. */
  151. public function getPlatform()
  152. {
  153. return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN;
  154. }
  155. /**
  156. * Set platform.
  157. *
  158. * @param int $platform
  159. *
  160. * @throws ZipException
  161. *
  162. * @return ZipEntry
  163. */
  164. public function setPlatform($platform)
  165. {
  166. $known = $platform !== self::UNKNOWN;
  167. if ($known) {
  168. if ($platform < 0x00 || $platform > 0xff) {
  169. throw new ZipException('Platform out of range');
  170. }
  171. $this->platform = $platform;
  172. } else {
  173. $this->platform = 0;
  174. }
  175. $this->setInit(self::BIT_PLATFORM, $known);
  176. return $this;
  177. }
  178. /**
  179. * @param int $mask
  180. *
  181. * @return bool
  182. */
  183. protected function isInit($mask)
  184. {
  185. return ($this->init & $mask) !== 0;
  186. }
  187. /**
  188. * @param int $mask
  189. * @param bool $init
  190. */
  191. protected function setInit($mask, $init)
  192. {
  193. if ($init) {
  194. $this->init |= $mask;
  195. } else {
  196. $this->init &= ~$mask;
  197. }
  198. }
  199. /**
  200. * Version needed to extract.
  201. *
  202. * @return int
  203. */
  204. public function getVersionNeededToExtract()
  205. {
  206. return $this->versionNeededToExtract;
  207. }
  208. /**
  209. * Set version needed to extract.
  210. *
  211. * @param int $version
  212. *
  213. * @return ZipEntry
  214. */
  215. public function setVersionNeededToExtract($version)
  216. {
  217. $this->versionNeededToExtract = $version;
  218. return $this;
  219. }
  220. /**
  221. * @return bool
  222. */
  223. public function isZip64ExtensionsRequired()
  224. {
  225. return $this->getCompressedSize() >= 0xffffffff
  226. || $this->getSize() >= 0xffffffff;
  227. }
  228. /**
  229. * Returns the compressed size of this entry.
  230. *
  231. * @see int
  232. */
  233. public function getCompressedSize()
  234. {
  235. return $this->compressedSize;
  236. }
  237. /**
  238. * Sets the compressed size of this entry.
  239. *
  240. * @param int $compressedSize the Compressed Size
  241. *
  242. * @return ZipEntry
  243. */
  244. public function setCompressedSize($compressedSize)
  245. {
  246. $this->compressedSize = $compressedSize;
  247. return $this;
  248. }
  249. /**
  250. * Returns the uncompressed size of this entry.
  251. *
  252. * @see ZipEntry::setCompressedSize
  253. */
  254. public function getSize()
  255. {
  256. return $this->size;
  257. }
  258. /**
  259. * Sets the uncompressed size of this entry.
  260. *
  261. * @param int $size the (Uncompressed) Size
  262. *
  263. * @return ZipEntry
  264. */
  265. public function setSize($size)
  266. {
  267. $this->size = $size;
  268. return $this;
  269. }
  270. /**
  271. * Return relative Offset Of Local File Header.
  272. *
  273. * @return int
  274. */
  275. public function getOffset()
  276. {
  277. return $this->offset;
  278. }
  279. /**
  280. * @param int $offset
  281. *
  282. * @return ZipEntry
  283. */
  284. public function setOffset($offset)
  285. {
  286. $this->offset = $offset;
  287. return $this;
  288. }
  289. /**
  290. * Returns the General Purpose Bit Flags.
  291. *
  292. * @return int
  293. */
  294. public function getGeneralPurposeBitFlags()
  295. {
  296. return $this->general & 0xffff;
  297. }
  298. /**
  299. * Sets the General Purpose Bit Flags.
  300. *
  301. * @param mixed $general
  302. *
  303. * @throws ZipException
  304. *
  305. * @return ZipEntry
  306. *
  307. * @var int general
  308. */
  309. public function setGeneralPurposeBitFlags($general)
  310. {
  311. if ($general < 0x0000 || $general > 0xffff) {
  312. throw new ZipException('general out of range');
  313. }
  314. $this->general = $general;
  315. if ($this->method === ZipFileInterface::METHOD_DEFLATED) {
  316. $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1);
  317. $bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2);
  318. if ($bit1 && !$bit2) {
  319. $this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION;
  320. } elseif (!$bit1 && $bit2) {
  321. $this->compressionLevel = ZipFileInterface::LEVEL_FAST;
  322. } elseif ($bit1 && $bit2) {
  323. $this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST;
  324. } else {
  325. $this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
  326. }
  327. }
  328. return $this;
  329. }
  330. /**
  331. * Returns true if and only if this ZIP entry is encrypted.
  332. *
  333. * @return bool
  334. */
  335. public function isEncrypted()
  336. {
  337. return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
  338. }
  339. /**
  340. * Returns the indexed General Purpose Bit Flag.
  341. *
  342. * @param int $mask
  343. *
  344. * @return bool
  345. */
  346. public function getGeneralPurposeBitFlag($mask)
  347. {
  348. return ($this->general & $mask) !== 0;
  349. }
  350. /**
  351. * Sets the encryption property to false and removes any other
  352. * encryption artifacts.
  353. *
  354. * @throws ZipException
  355. *
  356. * @return ZipEntry
  357. */
  358. public function disableEncryption()
  359. {
  360. $this->setEncrypted(false);
  361. $headerId = WinZipAesEntryExtraField::getHeaderId();
  362. if (isset($this->extraFieldsCollection[$headerId])) {
  363. /** @var WinZipAesEntryExtraField $field */
  364. $field = $this->extraFieldsCollection[$headerId];
  365. if ($this->getMethod() === self::METHOD_WINZIP_AES) {
  366. $this->setMethod($field === null ? self::UNKNOWN : $field->getMethod());
  367. }
  368. unset($this->extraFieldsCollection[$headerId]);
  369. }
  370. $this->password = null;
  371. return $this;
  372. }
  373. /**
  374. * Sets the encryption flag for this ZIP entry.
  375. *
  376. * @param bool $encrypted
  377. *
  378. * @return ZipEntry
  379. */
  380. public function setEncrypted($encrypted)
  381. {
  382. $encrypted = (bool) $encrypted;
  383. $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
  384. return $this;
  385. }
  386. /**
  387. * Returns the compression method for this entry.
  388. *
  389. * @return int
  390. */
  391. public function getMethod()
  392. {
  393. $isInit = $this->isInit(self::BIT_METHOD);
  394. return $isInit ?
  395. $this->method & 0xffff :
  396. self::UNKNOWN;
  397. }
  398. /**
  399. * Sets the compression method for this entry.
  400. *
  401. * @param int $method
  402. *
  403. * @throws ZipException if method is not STORED, DEFLATED, BZIP2 or UNKNOWN
  404. *
  405. * @return ZipEntry
  406. */
  407. public function setMethod($method)
  408. {
  409. if ($method === self::UNKNOWN) {
  410. $this->method = $method;
  411. $this->setInit(self::BIT_METHOD, false);
  412. return $this;
  413. }
  414. if ($method < 0x0000 || $method > 0xffff) {
  415. throw new ZipException('method out of range: ' . $method);
  416. }
  417. switch ($method) {
  418. case self::METHOD_WINZIP_AES:
  419. case ZipFileInterface::METHOD_STORED:
  420. case ZipFileInterface::METHOD_DEFLATED:
  421. case ZipFileInterface::METHOD_BZIP2:
  422. $this->method = $method;
  423. $this->setInit(self::BIT_METHOD, true);
  424. break;
  425. default:
  426. throw new ZipException($this->name . " (unsupported compression method {$method})");
  427. }
  428. return $this;
  429. }
  430. /**
  431. * Get Unix Timestamp.
  432. *
  433. * @return int
  434. */
  435. public function getTime()
  436. {
  437. if (!$this->isInit(self::BIT_DATE_TIME)) {
  438. return self::UNKNOWN;
  439. }
  440. return DateTimeConverter::toUnixTimestamp($this->getDosTime());
  441. }
  442. /**
  443. * Get Dos Time.
  444. *
  445. * @return int
  446. */
  447. public function getDosTime()
  448. {
  449. return $this->dosTime;
  450. }
  451. /**
  452. * Set Dos Time.
  453. *
  454. * @param int $dosTime
  455. *
  456. * @throws ZipException
  457. */
  458. public function setDosTime($dosTime)
  459. {
  460. $dosTime = (int) $dosTime;
  461. if ($dosTime < 0x00000000 || $dosTime > 0xffffffff) {
  462. throw new ZipException('DosTime out of range');
  463. }
  464. $this->dosTime = $dosTime;
  465. $this->setInit(self::BIT_DATE_TIME, true);
  466. }
  467. /**
  468. * Set time from unix timestamp.
  469. *
  470. * @param int $unixTimestamp
  471. *
  472. * @throws ZipException
  473. *
  474. * @return ZipEntry
  475. */
  476. public function setTime($unixTimestamp)
  477. {
  478. $known = $unixTimestamp !== self::UNKNOWN;
  479. if ($known) {
  480. $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
  481. } else {
  482. $this->dosTime = 0;
  483. }
  484. $this->setInit(self::BIT_DATE_TIME, $known);
  485. return $this;
  486. }
  487. /**
  488. * Returns the external file attributes.
  489. *
  490. * @return int the external file attributes
  491. */
  492. public function getExternalAttributes()
  493. {
  494. if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) {
  495. return $this->isDirectory() ? 0x10 : 0;
  496. }
  497. return $this->externalAttributes;
  498. }
  499. /**
  500. * Sets the external file attributes.
  501. *
  502. * @param int $externalAttributes the external file attributes
  503. *
  504. * @return ZipEntry
  505. */
  506. public function setExternalAttributes($externalAttributes)
  507. {
  508. $known = $externalAttributes !== self::UNKNOWN;
  509. if ($known) {
  510. $this->externalAttributes = $externalAttributes;
  511. } else {
  512. $this->externalAttributes = 0;
  513. }
  514. $this->setInit(self::BIT_EXTERNAL_ATTR, $known);
  515. return $this;
  516. }
  517. /**
  518. * Returns true if and only if this ZIP entry represents a directory entry
  519. * (i.e. end with '/').
  520. *
  521. * @return bool
  522. */
  523. public function isDirectory()
  524. {
  525. return StringUtil::endsWith($this->name, '/');
  526. }
  527. /**
  528. * @return ExtraFieldsCollection
  529. */
  530. public function &getExtraFieldsCollection()
  531. {
  532. return $this->extraFieldsCollection;
  533. }
  534. /**
  535. * Returns a protective copy of the serialized Extra Fields.
  536. *
  537. * @throws ZipException
  538. *
  539. * @return string
  540. */
  541. public function getExtra()
  542. {
  543. return ExtraFieldsFactory::createSerializedData($this->extraFieldsCollection);
  544. }
  545. /**
  546. * Sets the serialized Extra Fields by making a protective copy.
  547. * Note that this method parses the serialized Extra Fields according to
  548. * the ZIP File Format Specification and limits its size to 64 KB.
  549. * Therefore, this property cannot not be used to hold arbitrary
  550. * (application) data.
  551. * Consider storing such data in a separate entry instead.
  552. *
  553. * @param string $data the byte array holding the serialized Extra Fields
  554. *
  555. * @throws ZipException if the serialized Extra Fields exceed 64 KB
  556. */
  557. public function setExtra($data)
  558. {
  559. $this->extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($data, $this);
  560. }
  561. /**
  562. * Returns comment entry.
  563. *
  564. * @return string
  565. */
  566. public function getComment()
  567. {
  568. return $this->comment !== null ? $this->comment : '';
  569. }
  570. /**
  571. * Set entry comment.
  572. *
  573. * @param $comment
  574. *
  575. * @throws ZipException
  576. *
  577. * @return ZipEntry
  578. */
  579. public function setComment($comment)
  580. {
  581. if ($comment !== null) {
  582. $commentLength = \strlen($comment);
  583. if ($commentLength < 0x0000 || $commentLength > 0xffff) {
  584. throw new ZipException('Comment too long');
  585. }
  586. }
  587. $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true);
  588. $this->comment = $comment;
  589. return $this;
  590. }
  591. /**
  592. * @return bool
  593. */
  594. public function isDataDescriptorRequired()
  595. {
  596. return ($this->getCrc() | $this->getCompressedSize() | $this->getSize()) === self::UNKNOWN;
  597. }
  598. /**
  599. * Return crc32 content or 0 for WinZip AES v2.
  600. *
  601. * @return int
  602. */
  603. public function getCrc()
  604. {
  605. return $this->crc;
  606. }
  607. /**
  608. * Set crc32 content.
  609. *
  610. * @param int $crc
  611. *
  612. * @return ZipEntry
  613. */
  614. public function setCrc($crc)
  615. {
  616. $this->crc = $crc;
  617. $this->setInit(self::BIT_CRC, true);
  618. return $this;
  619. }
  620. /**
  621. * @return string
  622. */
  623. public function getPassword()
  624. {
  625. return $this->password;
  626. }
  627. /**
  628. * Set password and encryption method from entry.
  629. *
  630. * @param string $password
  631. * @param int|null $encryptionMethod
  632. *
  633. * @throws ZipException
  634. *
  635. * @return ZipEntry
  636. */
  637. public function setPassword($password, $encryptionMethod = null)
  638. {
  639. $this->password = $password;
  640. if ($encryptionMethod !== null) {
  641. $this->setEncryptionMethod($encryptionMethod);
  642. }
  643. if (!empty($this->password)) {
  644. $this->setEncrypted(true);
  645. } else {
  646. $this->disableEncryption();
  647. }
  648. return $this;
  649. }
  650. /**
  651. * @return int
  652. */
  653. public function getEncryptionMethod()
  654. {
  655. return $this->encryptionMethod;
  656. }
  657. /**
  658. * Set encryption method.
  659. *
  660. * @param int $encryptionMethod
  661. *
  662. * @throws ZipException
  663. *
  664. * @return ZipEntry
  665. *
  666. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
  667. * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
  668. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
  669. * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
  670. */
  671. public function setEncryptionMethod($encryptionMethod)
  672. {
  673. if ($encryptionMethod !== null) {
  674. if (
  675. $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
  676. && $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
  677. && $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
  678. && $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
  679. ) {
  680. throw new ZipException('Invalid encryption method');
  681. }
  682. $this->encryptionMethod = $encryptionMethod;
  683. }
  684. return $this;
  685. }
  686. /**
  687. * @return int
  688. */
  689. public function getCompressionLevel()
  690. {
  691. return $this->compressionLevel;
  692. }
  693. /**
  694. * @param int $compressionLevel
  695. *
  696. * @return ZipEntry
  697. */
  698. public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION)
  699. {
  700. if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ||
  701. $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION
  702. ) {
  703. throw new InvalidArgumentException(
  704. 'Invalid compression level. Minimum level ' .
  705. ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION
  706. );
  707. }
  708. $this->compressionLevel = $compressionLevel;
  709. return $this;
  710. }
  711. /**
  712. * Clone extra fields.
  713. */
  714. public function __clone()
  715. {
  716. $this->extraFieldsCollection = clone $this->extraFieldsCollection;
  717. }
  718. }