| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- <?php
- namespace PhpZip\Model;
- use PhpZip\Exception\InvalidArgumentException;
- use PhpZip\Exception\ZipException;
- use PhpZip\Exception\ZipNotFoundEntry;
- use PhpZip\Model\Entry\ZipNewStringEntry;
- use PhpZip\Model\Entry\ZipReadEntry;
- use PhpZip\ZipFile;
- /**
- * Read Central Directory
- *
- * @author Ne-Lexa alexey@nelexa.ru
- * @license MIT
- */
- class CentralDirectory
- {
- /** Central File Header signature. */
- const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
- /**
- * @var EndOfCentralDirectory End of Central Directory
- */
- private $endOfCentralDirectory;
- /**
- * @var ZipEntry[] Maps entry names to zip entries.
- */
- private $entries = [];
- /**
- * @var ZipEntry[] New and modified entries
- */
- private $modifiedEntries = [];
- /**
- * @var int Default compression level for the methods DEFLATED and BZIP2.
- */
- private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
- /**
- * @var int|null ZipAlign setting
- */
- private $zipAlign;
- /**
- * @var string New password
- */
- private $password;
- /**
- * @var int
- */
- private $encryptionMethod;
- /**
- * @var bool
- */
- private $clearPassword;
- public function __construct()
- {
- $this->endOfCentralDirectory = new EndOfCentralDirectory();
- }
- /**
- * Reads the central directory from the given seekable byte channel
- * and populates the internal tables with ZipEntry instances.
- *
- * The ZipEntry's will know all data that can be obtained from the
- * central directory alone, but not the data that requires the local
- * file header or additional data to be read.
- *
- * @param resource $inputStream
- * @throws ZipException
- */
- public function mountCentralDirectory($inputStream)
- {
- $this->modifiedEntries = [];
- $this->checkZipFileSignature($inputStream);
- $this->endOfCentralDirectory->findCentralDirectory($inputStream);
- $numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
- $entries = [];
- for (; $numEntries > 0; $numEntries--) {
- $entry = new ZipReadEntry($inputStream);
- $entry->setCentralDirectory($this);
- // Re-load virtual offset after ZIP64 Extended Information
- // Extra Field may have been parsed, map it to the real
- // offset and conditionally update the preamble size from it.
- $lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset());
- if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
- $this->endOfCentralDirectory->setPreamble($lfhOff);
- }
- $entries[$entry->getName()] = $entry;
- }
- if (0 !== $numEntries % 0x10000) {
- throw new ZipException("Expected " . abs($numEntries) .
- ($numEntries > 0 ? " more" : " less") .
- " entries in the Central Directory!");
- }
- $this->entries = $entries;
- if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
- assert(0 === $numEntries);
- $this->checkZipFileSignature($inputStream);
- }
- }
- /**
- * Check zip file signature
- *
- * @param resource $inputStream
- * @throws ZipException if this not .ZIP file.
- */
- private function checkZipFileSignature($inputStream)
- {
- rewind($inputStream);
- // Constraint: A ZIP file must start with a Local File Header
- // or a (ZIP64) End Of Central Directory Record if it's empty.
- $signatureBytes = fread($inputStream, 4);
- if (strlen($signatureBytes) < 4) {
- throw new ZipException("Invalid zip file.");
- }
- $signature = unpack('V', $signatureBytes)[1];
- if (
- ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
- && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
- && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
- ) {
- throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
- }
- }
- /**
- * Set compression method for new or rewrites entries.
- * @param int $compressionLevel
- * @throws InvalidArgumentException
- * @see ZipFile::LEVEL_DEFAULT_COMPRESSION
- * @see ZipFile::LEVEL_BEST_SPEED
- * @see ZipFile::LEVEL_BEST_COMPRESSION
- */
- public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
- {
- if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
- $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
- ) {
- throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
- ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
- }
- $this->compressionLevel = $compressionLevel;
- }
- /**
- * @return ZipEntry[]
- */
- public function &getEntries()
- {
- return $this->entries;
- }
- /**
- * @param string $entryName
- * @return ZipEntry
- * @throws ZipNotFoundEntry
- */
- public function getEntry($entryName)
- {
- if (!isset($this->entries[$entryName])) {
- throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
- }
- return $this->entries[$entryName];
- }
- /**
- * @return EndOfCentralDirectory
- */
- public function getEndOfCentralDirectory()
- {
- return $this->endOfCentralDirectory;
- }
- public function getArchiveComment()
- {
- return null === $this->endOfCentralDirectory->getComment() ?
- '' :
- $this->endOfCentralDirectory->getComment();
- }
- /**
- * Set entry comment
- * @param string $entryName
- * @param string|null $comment
- * @throws ZipNotFoundEntry
- */
- public function setEntryComment($entryName, $comment)
- {
- if (isset($this->modifiedEntries[$entryName])) {
- $this->modifiedEntries[$entryName]->setComment($comment);
- } elseif (isset($this->entries[$entryName])) {
- $entry = clone $this->entries[$entryName];
- $entry->setComment($comment);
- $this->putInModified($entryName, $entry);
- } else {
- throw new ZipNotFoundEntry("Not found entry " . $entryName);
- }
- }
- /**
- * @param string|null $password
- * @param int|null $encryptionMethod
- */
- public function setNewPassword($password, $encryptionMethod = null)
- {
- $this->password = $password;
- $this->encryptionMethod = $encryptionMethod;
- $this->clearPassword = $password === null;
- }
- /**
- * @return int|null
- */
- public function getZipAlign()
- {
- return $this->zipAlign;
- }
- /**
- * @param int|null $zipAlign
- */
- public function setZipAlign($zipAlign = null)
- {
- if (null === $zipAlign) {
- $this->zipAlign = null;
- return;
- }
- $this->zipAlign = (int)$zipAlign;
- }
- /**
- * Put modification or new entries.
- *
- * @param $entryName
- * @param ZipEntry $entry
- */
- public function putInModified($entryName, ZipEntry $entry)
- {
- $this->modifiedEntries[$entryName] = $entry;
- }
- /**
- * @param string $entryName
- * @throws ZipNotFoundEntry
- */
- public function deleteEntry($entryName)
- {
- if (isset($this->entries[$entryName])) {
- $this->modifiedEntries[$entryName] = null;
- } elseif (isset($this->modifiedEntries[$entryName])) {
- unset($this->modifiedEntries[$entryName]);
- } else {
- throw new ZipNotFoundEntry("Not found entry " . $entryName);
- }
- }
- /**
- * @param string $regexPattern
- * @return bool
- */
- public function deleteEntriesFromRegex($regexPattern)
- {
- $count = 0;
- foreach ($this->modifiedEntries as $entryName => &$entry) {
- if (preg_match($regexPattern, $entryName)) {
- unset($entry);
- $count++;
- }
- }
- foreach ($this->entries as $entryName => $entry) {
- if (preg_match($regexPattern, $entryName)) {
- $this->modifiedEntries[$entryName] = null;
- $count++;
- }
- }
- return $count > 0;
- }
- /**
- * @param string $oldName
- * @param string $newName
- * @throws InvalidArgumentException
- * @throws ZipNotFoundEntry
- */
- public function rename($oldName, $newName)
- {
- $oldName = (string)$oldName;
- $newName = (string)$newName;
- if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
- throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
- }
- if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
- $newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
- $this->modifiedEntries[$oldName] :
- $this->entries[$oldName]);
- $newEntry->setName($newName);
- $this->modifiedEntries[$oldName] = null;
- $this->modifiedEntries[$newName] = $newEntry;
- return;
- }
- throw new ZipNotFoundEntry("Not found entry " . $oldName);
- }
- /**
- * Delete all entries.
- */
- public function deleteAll()
- {
- $this->modifiedEntries = [];
- foreach ($this->entries as $entry) {
- $this->modifiedEntries[$entry->getName()] = null;
- }
- }
- /**
- * @param resource $outputStream
- */
- public function writeArchive($outputStream)
- {
- /**
- * @var ZipEntry[] $memoryEntriesResult
- */
- $memoryEntriesResult = [];
- foreach ($this->entries as $entryName => $entry) {
- if (isset($this->modifiedEntries[$entryName])) continue;
- if (
- (null !== $this->password || $this->clearPassword) &&
- $entry->isEncrypted() &&
- $entry->getPassword() !== null &&
- (
- $entry->getPassword() !== $this->password ||
- $entry->getEncryptionMethod() !== $this->encryptionMethod
- )
- ) {
- $prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
- $prototypeEntry->setName($entry->getName());
- $prototypeEntry->setMethod($entry->getMethod());
- $prototypeEntry->setTime($entry->getTime());
- $prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
- $prototypeEntry->setExtra($entry->getExtra());
- $prototypeEntry->setPassword($this->password, $this->encryptionMethod);
- if ($this->clearPassword) {
- $prototypeEntry->clearEncryption();
- }
- } else {
- $prototypeEntry = clone $entry;
- }
- $memoryEntriesResult[$entryName] = $prototypeEntry;
- }
- foreach ($this->modifiedEntries as $entryName => $outputEntry) {
- if (null === $outputEntry) { // remove marked entry
- unset($memoryEntriesResult[$entryName]);
- } else {
- if (null !== $this->password) {
- $outputEntry->setPassword($this->password, $this->encryptionMethod);
- }
- $memoryEntriesResult[$entryName] = $outputEntry;
- }
- }
- foreach ($memoryEntriesResult as $key => $outputEntry) {
- $outputEntry->setCentralDirectory($this);
- $outputEntry->writeEntry($outputStream);
- }
- $centralDirectoryOffset = ftell($outputStream);
- foreach ($memoryEntriesResult as $key => $outputEntry) {
- if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
- unset($memoryEntriesResult[$key]);
- }
- }
- $centralDirectoryEntries = sizeof($memoryEntriesResult);
- $this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
- $outputStream,
- $centralDirectoryEntries,
- $centralDirectoryOffset
- );
- }
- /**
- * Writes a Central File Header record.
- *
- * @param resource $outputStream
- * @param ZipEntry $entry
- * @return bool false if and only if the record has been skipped,
- * i.e. not written for some other reason than an I/O error.
- */
- private function writeCentralFileHeader($outputStream, ZipEntry $entry)
- {
- $compressedSize = $entry->getCompressedSize();
- $size = $entry->getSize();
- // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
- // UNKNOWN!
- if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
- return false;
- }
- $extra = $entry->getExtra();
- $extraSize = strlen($extra);
- $commentLength = strlen($entry->getComment());
- fwrite(
- $outputStream,
- pack(
- 'VvvvvVVVVvvvvvVV',
- // central file header signature 4 bytes (0x02014b50)
- self::CENTRAL_FILE_HEADER_SIG,
- // version made by 2 bytes
- ($entry->getPlatform() << 8) | 63,
- // version needed to extract 2 bytes
- $entry->getVersionNeededToExtract(),
- // general purpose bit flag 2 bytes
- $entry->getGeneralPurposeBitFlags(),
- // compression method 2 bytes
- $entry->getMethod(),
- // last mod file datetime 4 bytes
- $entry->getTime(),
- // crc-32 4 bytes
- $entry->getCrc(),
- // compressed size 4 bytes
- $entry->getCompressedSize(),
- // uncompressed size 4 bytes
- $entry->getSize(),
- // file name length 2 bytes
- strlen($entry->getName()),
- // extra field length 2 bytes
- $extraSize,
- // file comment length 2 bytes
- $commentLength,
- // disk number start 2 bytes
- 0,
- // internal file attributes 2 bytes
- 0,
- // external file attributes 4 bytes
- $entry->getExternalAttributes(),
- // relative offset of local header 4 bytes
- $entry->getOffset()
- )
- );
- // file name (variable size)
- fwrite($outputStream, $entry->getName());
- if (0 < $extraSize) {
- // extra field (variable size)
- fwrite($outputStream, $extra);
- }
- if (0 < $commentLength) {
- // file comment (variable size)
- fwrite($outputStream, $entry->getComment());
- }
- return true;
- }
- public function release()
- {
- unset($this->entries);
- unset($this->modifiedEntries);
- }
- function __destruct()
- {
- $this->release();
- }
- }
|