ZipAbstractEntry.php 19 KB

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