CentralDirectory.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. namespace PhpZip\Model;
  3. use PhpZip\Exception\InvalidArgumentException;
  4. use PhpZip\Exception\ZipException;
  5. use PhpZip\Exception\ZipNotFoundEntry;
  6. use PhpZip\Model\Entry\ZipNewStringEntry;
  7. use PhpZip\Model\Entry\ZipReadEntry;
  8. use PhpZip\ZipFile;
  9. /**
  10. * Read Central Directory
  11. *
  12. * @author Ne-Lexa alexey@nelexa.ru
  13. * @license MIT
  14. */
  15. class CentralDirectory
  16. {
  17. /** Central File Header signature. */
  18. const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
  19. /**
  20. * @var EndOfCentralDirectory End of Central Directory
  21. */
  22. private $endOfCentralDirectory;
  23. /**
  24. * @var ZipEntry[] Maps entry names to zip entries.
  25. */
  26. private $entries = [];
  27. /**
  28. * @var ZipEntry[] New and modified entries
  29. */
  30. private $modifiedEntries = [];
  31. /**
  32. * @var int Default compression level for the methods DEFLATED and BZIP2.
  33. */
  34. private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
  35. /**
  36. * @var int|null ZipAlign setting
  37. */
  38. private $zipAlign;
  39. /**
  40. * @var string New password
  41. */
  42. private $password;
  43. /**
  44. * @var int
  45. */
  46. private $encryptionMethod;
  47. /**
  48. * @var bool
  49. */
  50. private $clearPassword;
  51. public function __construct()
  52. {
  53. $this->endOfCentralDirectory = new EndOfCentralDirectory();
  54. }
  55. /**
  56. * Reads the central directory from the given seekable byte channel
  57. * and populates the internal tables with ZipEntry instances.
  58. *
  59. * The ZipEntry's will know all data that can be obtained from the
  60. * central directory alone, but not the data that requires the local
  61. * file header or additional data to be read.
  62. *
  63. * @param resource $inputStream
  64. * @throws ZipException
  65. */
  66. public function mountCentralDirectory($inputStream)
  67. {
  68. $this->modifiedEntries = [];
  69. $this->checkZipFileSignature($inputStream);
  70. $this->endOfCentralDirectory->findCentralDirectory($inputStream);
  71. $numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
  72. $entries = [];
  73. for (; $numEntries > 0; $numEntries--) {
  74. $entry = new ZipReadEntry($inputStream);
  75. $entry->setCentralDirectory($this);
  76. // Re-load virtual offset after ZIP64 Extended Information
  77. // Extra Field may have been parsed, map it to the real
  78. // offset and conditionally update the preamble size from it.
  79. $lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset());
  80. if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
  81. $this->endOfCentralDirectory->setPreamble($lfhOff);
  82. }
  83. $entries[$entry->getName()] = $entry;
  84. }
  85. if (0 !== $numEntries % 0x10000) {
  86. throw new ZipException("Expected " . abs($numEntries) .
  87. ($numEntries > 0 ? " more" : " less") .
  88. " entries in the Central Directory!");
  89. }
  90. $this->entries = $entries;
  91. if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
  92. assert(0 === $numEntries);
  93. $this->checkZipFileSignature($inputStream);
  94. }
  95. }
  96. /**
  97. * Check zip file signature
  98. *
  99. * @param resource $inputStream
  100. * @throws ZipException if this not .ZIP file.
  101. */
  102. private function checkZipFileSignature($inputStream)
  103. {
  104. rewind($inputStream);
  105. // Constraint: A ZIP file must start with a Local File Header
  106. // or a (ZIP64) End Of Central Directory Record if it's empty.
  107. $signatureBytes = fread($inputStream, 4);
  108. if (strlen($signatureBytes) < 4) {
  109. throw new ZipException("Invalid zip file.");
  110. }
  111. $signature = unpack('V', $signatureBytes)[1];
  112. if (
  113. ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
  114. && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
  115. && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
  116. ) {
  117. throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
  118. }
  119. }
  120. /**
  121. * Set compression method for new or rewrites entries.
  122. * @param int $compressionLevel
  123. * @throws InvalidArgumentException
  124. * @see ZipFile::LEVEL_DEFAULT_COMPRESSION
  125. * @see ZipFile::LEVEL_BEST_SPEED
  126. * @see ZipFile::LEVEL_BEST_COMPRESSION
  127. */
  128. public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
  129. {
  130. if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
  131. $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
  132. ) {
  133. throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
  134. ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
  135. }
  136. $this->compressionLevel = $compressionLevel;
  137. }
  138. /**
  139. * @return ZipEntry[]
  140. */
  141. public function &getEntries()
  142. {
  143. return $this->entries;
  144. }
  145. /**
  146. * @param string $entryName
  147. * @return ZipEntry
  148. * @throws ZipNotFoundEntry
  149. */
  150. public function getEntry($entryName)
  151. {
  152. if (!isset($this->entries[$entryName])) {
  153. throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
  154. }
  155. return $this->entries[$entryName];
  156. }
  157. /**
  158. * @return EndOfCentralDirectory
  159. */
  160. public function getEndOfCentralDirectory()
  161. {
  162. return $this->endOfCentralDirectory;
  163. }
  164. public function getArchiveComment()
  165. {
  166. return null === $this->endOfCentralDirectory->getComment() ?
  167. '' :
  168. $this->endOfCentralDirectory->getComment();
  169. }
  170. /**
  171. * Set entry comment
  172. * @param string $entryName
  173. * @param string|null $comment
  174. * @throws ZipNotFoundEntry
  175. */
  176. public function setEntryComment($entryName, $comment)
  177. {
  178. if (isset($this->modifiedEntries[$entryName])) {
  179. $this->modifiedEntries[$entryName]->setComment($comment);
  180. } elseif (isset($this->entries[$entryName])) {
  181. $entry = clone $this->entries[$entryName];
  182. $entry->setComment($comment);
  183. $this->putInModified($entryName, $entry);
  184. } else {
  185. throw new ZipNotFoundEntry("Not found entry " . $entryName);
  186. }
  187. }
  188. /**
  189. * @param string|null $password
  190. * @param int|null $encryptionMethod
  191. */
  192. public function setNewPassword($password, $encryptionMethod = null)
  193. {
  194. $this->password = $password;
  195. $this->encryptionMethod = $encryptionMethod;
  196. $this->clearPassword = $password === null;
  197. }
  198. /**
  199. * @return int|null
  200. */
  201. public function getZipAlign()
  202. {
  203. return $this->zipAlign;
  204. }
  205. /**
  206. * @param int|null $zipAlign
  207. */
  208. public function setZipAlign($zipAlign = null)
  209. {
  210. if (null === $zipAlign) {
  211. $this->zipAlign = null;
  212. return;
  213. }
  214. $this->zipAlign = (int)$zipAlign;
  215. }
  216. /**
  217. * Put modification or new entries.
  218. *
  219. * @param $entryName
  220. * @param ZipEntry $entry
  221. */
  222. public function putInModified($entryName, ZipEntry $entry)
  223. {
  224. $this->modifiedEntries[$entryName] = $entry;
  225. }
  226. /**
  227. * @param string $entryName
  228. * @throws ZipNotFoundEntry
  229. */
  230. public function deleteEntry($entryName)
  231. {
  232. if (isset($this->entries[$entryName])) {
  233. $this->modifiedEntries[$entryName] = null;
  234. } elseif (isset($this->modifiedEntries[$entryName])) {
  235. unset($this->modifiedEntries[$entryName]);
  236. } else {
  237. throw new ZipNotFoundEntry("Not found entry " . $entryName);
  238. }
  239. }
  240. /**
  241. * @param string $regexPattern
  242. * @return bool
  243. */
  244. public function deleteEntriesFromRegex($regexPattern)
  245. {
  246. $count = 0;
  247. foreach ($this->modifiedEntries as $entryName => &$entry) {
  248. if (preg_match($regexPattern, $entryName)) {
  249. unset($entry);
  250. $count++;
  251. }
  252. }
  253. foreach ($this->entries as $entryName => $entry) {
  254. if (preg_match($regexPattern, $entryName)) {
  255. $this->modifiedEntries[$entryName] = null;
  256. $count++;
  257. }
  258. }
  259. return $count > 0;
  260. }
  261. /**
  262. * @param string $oldName
  263. * @param string $newName
  264. * @throws InvalidArgumentException
  265. * @throws ZipNotFoundEntry
  266. */
  267. public function rename($oldName, $newName)
  268. {
  269. $oldName = (string)$oldName;
  270. $newName = (string)$newName;
  271. if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
  272. throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
  273. }
  274. if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
  275. $newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
  276. $this->modifiedEntries[$oldName] :
  277. $this->entries[$oldName]);
  278. $newEntry->setName($newName);
  279. $this->modifiedEntries[$oldName] = null;
  280. $this->modifiedEntries[$newName] = $newEntry;
  281. return;
  282. }
  283. throw new ZipNotFoundEntry("Not found entry " . $oldName);
  284. }
  285. /**
  286. * Delete all entries.
  287. */
  288. public function deleteAll()
  289. {
  290. $this->modifiedEntries = [];
  291. foreach ($this->entries as $entry) {
  292. $this->modifiedEntries[$entry->getName()] = null;
  293. }
  294. }
  295. /**
  296. * @param resource $outputStream
  297. */
  298. public function writeArchive($outputStream)
  299. {
  300. /**
  301. * @var ZipEntry[] $memoryEntriesResult
  302. */
  303. $memoryEntriesResult = [];
  304. foreach ($this->entries as $entryName => $entry) {
  305. if (isset($this->modifiedEntries[$entryName])) continue;
  306. if (
  307. (null !== $this->password || $this->clearPassword) &&
  308. $entry->isEncrypted() &&
  309. $entry->getPassword() !== null &&
  310. (
  311. $entry->getPassword() !== $this->password ||
  312. $entry->getEncryptionMethod() !== $this->encryptionMethod
  313. )
  314. ) {
  315. $prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
  316. $prototypeEntry->setName($entry->getName());
  317. $prototypeEntry->setMethod($entry->getMethod());
  318. $prototypeEntry->setTime($entry->getTime());
  319. $prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
  320. $prototypeEntry->setExtra($entry->getExtra());
  321. $prototypeEntry->setPassword($this->password, $this->encryptionMethod);
  322. if ($this->clearPassword) {
  323. $prototypeEntry->clearEncryption();
  324. }
  325. } else {
  326. $prototypeEntry = clone $entry;
  327. }
  328. $memoryEntriesResult[$entryName] = $prototypeEntry;
  329. }
  330. foreach ($this->modifiedEntries as $entryName => $outputEntry) {
  331. if (null === $outputEntry) { // remove marked entry
  332. unset($memoryEntriesResult[$entryName]);
  333. } else {
  334. if (null !== $this->password) {
  335. $outputEntry->setPassword($this->password, $this->encryptionMethod);
  336. }
  337. $memoryEntriesResult[$entryName] = $outputEntry;
  338. }
  339. }
  340. foreach ($memoryEntriesResult as $key => $outputEntry) {
  341. $outputEntry->setCentralDirectory($this);
  342. $outputEntry->writeEntry($outputStream);
  343. }
  344. $centralDirectoryOffset = ftell($outputStream);
  345. foreach ($memoryEntriesResult as $key => $outputEntry) {
  346. if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
  347. unset($memoryEntriesResult[$key]);
  348. }
  349. }
  350. $centralDirectoryEntries = sizeof($memoryEntriesResult);
  351. $this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
  352. $outputStream,
  353. $centralDirectoryEntries,
  354. $centralDirectoryOffset
  355. );
  356. }
  357. /**
  358. * Writes a Central File Header record.
  359. *
  360. * @param resource $outputStream
  361. * @param ZipEntry $entry
  362. * @return bool false if and only if the record has been skipped,
  363. * i.e. not written for some other reason than an I/O error.
  364. */
  365. private function writeCentralFileHeader($outputStream, ZipEntry $entry)
  366. {
  367. $compressedSize = $entry->getCompressedSize();
  368. $size = $entry->getSize();
  369. // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
  370. // UNKNOWN!
  371. if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
  372. return false;
  373. }
  374. $extra = $entry->getExtra();
  375. $extraSize = strlen($extra);
  376. $commentLength = strlen($entry->getComment());
  377. fwrite(
  378. $outputStream,
  379. pack(
  380. 'VvvvvVVVVvvvvvVV',
  381. // central file header signature 4 bytes (0x02014b50)
  382. self::CENTRAL_FILE_HEADER_SIG,
  383. // version made by 2 bytes
  384. ($entry->getPlatform() << 8) | 63,
  385. // version needed to extract 2 bytes
  386. $entry->getVersionNeededToExtract(),
  387. // general purpose bit flag 2 bytes
  388. $entry->getGeneralPurposeBitFlags(),
  389. // compression method 2 bytes
  390. $entry->getMethod(),
  391. // last mod file datetime 4 bytes
  392. $entry->getTime(),
  393. // crc-32 4 bytes
  394. $entry->getCrc(),
  395. // compressed size 4 bytes
  396. $entry->getCompressedSize(),
  397. // uncompressed size 4 bytes
  398. $entry->getSize(),
  399. // file name length 2 bytes
  400. strlen($entry->getName()),
  401. // extra field length 2 bytes
  402. $extraSize,
  403. // file comment length 2 bytes
  404. $commentLength,
  405. // disk number start 2 bytes
  406. 0,
  407. // internal file attributes 2 bytes
  408. 0,
  409. // external file attributes 4 bytes
  410. $entry->getExternalAttributes(),
  411. // relative offset of local header 4 bytes
  412. $entry->getOffset()
  413. )
  414. );
  415. // file name (variable size)
  416. fwrite($outputStream, $entry->getName());
  417. if (0 < $extraSize) {
  418. // extra field (variable size)
  419. fwrite($outputStream, $extra);
  420. }
  421. if (0 < $commentLength) {
  422. // file comment (variable size)
  423. fwrite($outputStream, $entry->getComment());
  424. }
  425. return true;
  426. }
  427. public function release()
  428. {
  429. unset($this->entries);
  430. unset($this->modifiedEntries);
  431. }
  432. function __destruct()
  433. {
  434. $this->release();
  435. }
  436. }