ZipNewEntry.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. namespace PhpZip\Model\Entry;
  3. use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
  4. use PhpZip\Crypto\WinZipAesEngine;
  5. use PhpZip\Exception\ZipException;
  6. use PhpZip\Extra\WinZipAesEntryExtraField;
  7. use PhpZip\Model\ZipEntry;
  8. use PhpZip\Util\PackUtil;
  9. use PhpZip\ZipFile;
  10. /**
  11. * Abstract class for new zip entry.
  12. *
  13. * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  14. * @author Ne-Lexa alexey@nelexa.ru
  15. * @license MIT
  16. */
  17. abstract class ZipNewEntry extends ZipAbstractEntry
  18. {
  19. /**
  20. * Default compression level for bzip2
  21. */
  22. const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
  23. /**
  24. * Version needed to extract.
  25. *
  26. * @return int
  27. */
  28. public function getVersionNeededToExtract()
  29. {
  30. $method = $this->getMethod();
  31. return self::METHOD_WINZIP_AES === $method ? 51 :
  32. (ZipFile::METHOD_BZIP2 === $method ? 46 :
  33. ($this->isZip64ExtensionsRequired() ? 45 :
  34. (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10)
  35. )
  36. );
  37. }
  38. /**
  39. * Write local file header, encryption header, file data and data descriptor to output stream.
  40. *
  41. * @param resource $outputStream
  42. * @throws ZipException
  43. */
  44. public function writeEntry($outputStream)
  45. {
  46. $nameLength = strlen($this->getName());
  47. $size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment());
  48. if (0xffff < $size) {
  49. throw new ZipException($this->getName()
  50. . " (the total size of "
  51. . $size
  52. . " bytes for the name, extra fields and comment exceeds the maximum size of "
  53. . 0xffff . " bytes)");
  54. }
  55. if (self::UNKNOWN === $this->getPlatform()) {
  56. $this->setPlatform(self::PLATFORM_UNIX);
  57. }
  58. if (self::UNKNOWN === $this->getTime()) {
  59. $this->setTime(time());
  60. }
  61. $method = $this->getMethod();
  62. if (self::UNKNOWN === $method) {
  63. $this->setMethod($method = ZipFile::METHOD_DEFLATED);
  64. }
  65. $skipCrc = false;
  66. $encrypted = $this->isEncrypted();
  67. $dd = $this->isDataDescriptorRequired();
  68. // Compose General Purpose Bit Flag.
  69. // See appendix D of PKWARE's ZIP File Format Specification.
  70. $utf8 = true;
  71. $general = ($encrypted ? self::GPBF_ENCRYPTED : 0)
  72. | ($dd ? self::GPBF_DATA_DESCRIPTOR : 0)
  73. | ($utf8 ? self::GPBF_UTF8 : 0);
  74. $entryContent = $this->getEntryContent();
  75. $this->setSize(strlen($entryContent));
  76. $this->setCrc(crc32($entryContent));
  77. if ($encrypted && null === $this->getPassword()) {
  78. throw new ZipException("Can not password from entry " . $this->getName());
  79. }
  80. if (
  81. $encrypted &&
  82. (
  83. self::METHOD_WINZIP_AES === $method ||
  84. $this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES
  85. )
  86. ) {
  87. $field = null;
  88. $method = $this->getMethod();
  89. $keyStrength = 256; // bits
  90. $compressedSize = $this->getCompressedSize();
  91. if (self::METHOD_WINZIP_AES === $method) {
  92. /**
  93. * @var WinZipAesEntryExtraField $field
  94. */
  95. $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
  96. if (null !== $field) {
  97. $method = $field->getMethod();
  98. if (self::UNKNOWN !== $compressedSize) {
  99. $compressedSize -= $field->getKeyStrength() / 2 // salt value
  100. + 2 // password verification value
  101. + 10; // authentication code
  102. }
  103. $this->setMethod($method);
  104. }
  105. }
  106. if (null === $field) {
  107. $field = new WinZipAesEntryExtraField();
  108. }
  109. $field->setKeyStrength($keyStrength);
  110. $field->setMethod($method);
  111. $size = $this->getSize();
  112. if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) {
  113. $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
  114. } else {
  115. $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
  116. $skipCrc = true;
  117. }
  118. $this->addExtraField($field);
  119. if (self::UNKNOWN !== $compressedSize) {
  120. $compressedSize += $field->getKeyStrength() / 2 // salt value
  121. + 2 // password verification value
  122. + 10; // authentication code
  123. $this->setCompressedSize($compressedSize);
  124. }
  125. if ($skipCrc) {
  126. $this->setCrc(0);
  127. }
  128. }
  129. switch ($method) {
  130. case ZipFile::METHOD_STORED:
  131. break;
  132. case ZipFile::METHOD_DEFLATED:
  133. $entryContent = gzdeflate($entryContent, $this->getCompressionLevel());
  134. break;
  135. case ZipFile::METHOD_BZIP2:
  136. $compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
  137. self::LEVEL_DEFAULT_BZIP2_COMPRESSION :
  138. $this->getCompressionLevel();
  139. $entryContent = bzcompress($entryContent, $compressionLevel);
  140. if (is_int($entryContent)) {
  141. throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
  142. }
  143. break;
  144. default:
  145. throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")");
  146. }
  147. if ($encrypted) {
  148. if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) {
  149. if ($skipCrc) {
  150. $this->setCrc(0);
  151. }
  152. $this->setMethod(self::METHOD_WINZIP_AES);
  153. /**
  154. * @var WinZipAesEntryExtraField $field
  155. */
  156. $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
  157. $winZipAesEngine = new WinZipAesEngine($this, $field);
  158. $entryContent = $winZipAesEngine->encrypt($entryContent);
  159. } elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
  160. $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
  161. $entryContent = $zipCryptoEngine->encrypt($entryContent);
  162. }
  163. }
  164. $compressedSize = strlen($entryContent);
  165. $this->setCompressedSize($compressedSize);
  166. $offset = ftell($outputStream);
  167. // Commit changes.
  168. $this->setGeneralPurposeBitFlags($general);
  169. $this->setOffset($offset);
  170. $extra = $this->getExtra();
  171. // zip align
  172. $padding = 0;
  173. $zipAlign = $this->getCentralDirectory()->getZipAlign();
  174. $extraLength = strlen($extra);
  175. if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
  176. $padding =
  177. (
  178. $zipAlign -
  179. (
  180. $offset +
  181. ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
  182. $nameLength + $extraLength
  183. ) % $zipAlign
  184. ) % $zipAlign;
  185. }
  186. fwrite(
  187. $outputStream,
  188. pack(
  189. 'VvvvVVVVvv',
  190. // local file header signature 4 bytes (0x04034b50)
  191. self::LOCAL_FILE_HEADER_SIG,
  192. // version needed to extract 2 bytes
  193. $this->getVersionNeededToExtract(),
  194. // general purpose bit flag 2 bytes
  195. $general,
  196. // compression method 2 bytes
  197. $this->getMethod(),
  198. // last mod file time 2 bytes
  199. // last mod file date 2 bytes
  200. $this->getTime(),
  201. // crc-32 4 bytes
  202. $dd ? 0 : $this->getCrc(),
  203. // compressed size 4 bytes
  204. $dd ? 0 : $this->getCompressedSize(),
  205. // uncompressed size 4 bytes
  206. $dd ? 0 : $this->getSize(),
  207. // file name length 2 bytes
  208. $nameLength,
  209. // extra field length 2 bytes
  210. $extraLength + $padding
  211. )
  212. );
  213. fwrite($outputStream, $this->getName());
  214. if ($extraLength > 0) {
  215. fwrite($outputStream, $extra);
  216. }
  217. if ($padding > 0) {
  218. fwrite($outputStream, str_repeat(chr(0), $padding));
  219. }
  220. if ($entryContent !== null) {
  221. fwrite($outputStream, $entryContent);
  222. }
  223. assert(self::UNKNOWN !== $this->getCrc());
  224. assert(self::UNKNOWN !== $this->getSize());
  225. if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
  226. // data descriptor signature 4 bytes (0x08074b50)
  227. // crc-32 4 bytes
  228. fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc()));
  229. // compressed size 4 or 8 bytes
  230. // uncompressed size 4 or 8 bytes
  231. if ($this->isZip64ExtensionsRequired()) {
  232. fwrite($outputStream, PackUtil::packLongLE($compressedSize));
  233. fwrite($outputStream, PackUtil::packLongLE($this->getSize()));
  234. } else {
  235. fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize()));
  236. }
  237. } elseif ($this->getCompressedSize() !== $compressedSize) {
  238. throw new ZipException($this->getName()
  239. . " (expected compressed entry size of "
  240. . $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
  241. }
  242. }
  243. }