From 45937d2620027d6c131fed5c499fadac926af970 Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Thu, 8 Sep 2016 19:18:53 +0300 Subject: [PATCH] Zip file create and modification --- .gitignore | 3 + README.md | 250 +++++++ composer.json | 25 + src/FilterFileIterator.php | 45 ++ src/ZipEntry.php | 905 ++++++++++++++++++++++++ src/ZipException.php | 7 + src/ZipFile.php | 1374 ++++++++++++++++++++++++++++++++++++ src/ZipUtils.php | 104 +++ tests/TestZipFile.php | 244 +++++++ tests/res/file.apk | Bin 0 -> 31742 bytes tests/res/private.pem | 28 + tests/res/public.pem | 21 + 12 files changed, 3006 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/FilterFileIterator.php create mode 100644 src/ZipEntry.php create mode 100644 src/ZipException.php create mode 100644 src/ZipFile.php create mode 100644 src/ZipUtils.php create mode 100644 tests/TestZipFile.php create mode 100644 tests/res/file.apk create mode 100644 tests/res/private.pem create mode 100644 tests/res/public.pem diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c097bab --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +*.iml +/.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..76526e2 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +## Documentation + +Create and manipulate zip archives. No use ZipArchive class and php-zip extension. + +### class \Nelexa\Zip\ZipFile +Initialization +```php +$zip = new \Nelexa\Zip\ZipFile(); +``` +Create archive +```php +$zip->create(); +``` +Open archive file +```php +$zip->open($filename); +``` +Open archive from string +```php +$zip->openFromString($string) +``` +Set password +```php +$zip->setPassword($password); +``` +List files +```php +$listFiles = $zip->getListFiles(); +``` +Get count files +```php +$countFiles = $zip->getCountFiles(); +``` +Add empty dir +```php +$zip->addEmptyDir($dirName); +``` +Add dir +```php +$directory = "/tmp"; +$ignoreFiles = array("xxx.file", "xxx2.file"); +$zip->addDir($directory); // add path /tmp to / +$zip->addDir($directory, "var/temp"); // add path /tmp to var/temp +$zip->addDir($directory, "var/temp", $ignoreFiles); // add path /tmp to var/temp and ignore files xxx.file and xxx2.file +``` +Add files from glob pattern +```php +$zip->addGlob("music/*.mp3"); // add all mp3 files +``` +Add files from regex pattern +```php +$zip->addPattern("~file[0-9]+\.jpg$~", "picture/"); +``` +Add file +```php +$zip->addFile($filename); +$zip->addFile($filename, $localName); +$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression +$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED); +``` +Add file from string +```php +$zip->addFromString($localName, $contents); +$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression +$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED); +``` +Update timestamp for all files +```php +$timestamp = time(); // now time +$zip->updateTimestamp($timestamp); +``` +Delete files from glob pattern +```php +$zip->deleteGlob("*.jpg"); // remove all jpg files +``` +Delete files from regex pattern +```php +$zip->deletePattern("~\.jpg$~i"); // remove all jpg files +``` +Delete file from index +```php +$zip->deleteIndex(0); +``` +Delete all files +```php +$zip->deleteAll(); +``` +Delete from file name +```php +$zip->deleteName($filename); +``` +Extract zip archive +```php +$zip->extractTo($toPath) +$zip->extractTo($toPath, array("file1", "file2")); // extract only files file1 and file2 +``` +Get archive comment +```php +$archiveComment = $zip->getArchiveComment(); +``` +Set archive comment +```php +$zip->setArchiveComment($comment) +``` +Get comment file from index +```php +$commentFile = $zip->getCommentIndex($index); +``` +Set comment file from index +```php +$zip->setCommentIndex($index, $comment); +``` +Get comment file from filename +```php +$commentFile = $zip->getCommentName($filename); +``` +Set comment file from filename +```php +$zip->setCommentName($name, $comment); +``` +Get file content from index +```php +$content = $zip->getFromIndex($index); +``` +Get file content from filename +```php +$content = $zip->getFromName($name); +``` +Get filename from index +```php +$filename = $zip->getNameIndex($index); +``` +Rename file from index +```php +$zip->renameIndex($index, $newFilename); +``` +Rename file from filename +```php +$zip->renameName($oldName, $newName); +``` +Get zip entries +```php +/** + * @var \Nelexa\Zip\ZipEntry[] $zipEntries + */ +$zipEntries = $zip->getZipEntries(); +``` +Get zip entry from index +```php +/** + * @var \Nelexa\Zip\ZipEntry $zipEntry + */ +$zipEntry = $zip->getZipEntryIndex($index); +``` +Get zip entry from filename +```php +/** + * @var \Nelexa\Zip\ZipEntry $zipEntry + */ +$zipEntry = $zip->getZipEntryName($name); +``` +Get info from index +```php +$info = $zip->statIndex($index); +// [ +// 'name' - filename +// 'index' - index number +// 'crc' - crc32 +// 'size' - uncompressed size +// 'mtime' - last modify date time +// 'comp_size' - compressed size +// 'comp_method' - compressed method +// ] +``` +Get info from name +```php +$info = $zip->statName($name); +// [ +// 'name' - filename +// 'index' - index number +// 'crc' - crc32 +// 'size' - uncompressed size +// 'mtime' - last modify date time +// 'comp_size' - compressed size +// 'comp_method' - compressed method +// ] +``` +Get info from all files +```php +$info = $zip->getExtendedListFiles(); +``` +Get output contents +```php +$content = $zip->output(); +``` +Save opened file +```php +$isSuccessSave = $zip->save(); +``` +Save file as +```php +$zip->saveAs($outputFile); +``` +Close archive +```php +$zip->close(); +``` + +### Example create zip archive +```php +$zip = new \Nelexa\Zip\ZipFile(); +$zip->create(); +$zip->addFile("README.md"); +$zip->addFile("README.md", "folder/README"); +$zip->addFromString("folder/file.txt", "File content"); +$zip->addEmptyDir("f/o/l/d/e/r"); +$zip->setArchiveComment("Archive comment"); +$zip->setCommentIndex(0, "Comment file with index 0"); +$zip->saveAs("output.zip"); +$zip->close(); + +// $ zipinfo output.zip +// Archive: output.zip +// Zip file size: 912 bytes, number of entries: 4 +// -rw---- 1.0 fat 387 b- defN README.md +// -rw---- 1.0 fat 387 b- defN folder/README +// -rw---- 1.0 fat 12 b- defN folder/file.txt +// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/ +// 4 files, 786 bytes uncompressed, 448 bytes compressed: 43.0% +``` + +### Example modification zip archive +```php +$zip = new \Nelexa\Zip\ZipFile(); +$zip->open("output.zip"); +$zip->addFromString("new-file", file_get_contents(__FILE__)); +$zip->saveAs("output2.zip"); +$zip->save(); +$zip->close(); + +// $ zipinfo output2.zip +// Archive: output2.zip +// Zip file size: 1331 bytes, number of entries: 5 +// -rw---- 1.0 fat 387 b- defN README.md +// -rw---- 1.0 fat 387 b- defN folder/README +// -rw---- 1.0 fat 12 b- defN folder/file.txt +// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/ +// -rw---- 1.0 fat 593 b- defN new-file +// 5 files, 1379 bytes uncompressed, 775 bytes compressed: 43.8% +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8ec18cf --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "nelexa/zip", + "description": "Zip create, modify and extract tool. Alternative ZipArchive.", + "type": "library", + "require-dev": { + "phpunit/phpunit": "^5.5" + }, + "license": "MIT", + "authors": [ + { + "name": "Ne-Lexa", + "email": "alexey@nelexa.ru" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=5.3", + "nelexa/buffer": "^1.0" + }, + "autoload": { + "psr-4": { + "Nelexa\\Zip\\": "src" + } + } +} diff --git a/src/FilterFileIterator.php b/src/FilterFileIterator.php new file mode 100644 index 0000000..7bc0fe4 --- /dev/null +++ b/src/FilterFileIterator.php @@ -0,0 +1,45 @@ +ignoreFiles = array_merge(self::$ignoreAlways, $ignoreFiles); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Check whether the current element of the iterator is acceptable + * @link http://php.net/manual/en/filteriterator.accept.php + * @return bool true if the current element is acceptable, otherwise false. + */ + public function accept() + { + /** + * @var \SplFileInfo $value + */ + $value = $this->current(); + $pathName = $value->getRealPath(); + foreach ($this->ignoreFiles AS $ignoreFile) { + if ($this->endsWith($pathName, $ignoreFile)) { + return false; + } + } + return true; + } + + function endsWith($haystack, $needle) + { + // search forward starting from end minus needle length characters + return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== FALSE); + } +} \ No newline at end of file diff --git a/src/ZipEntry.php b/src/ZipEntry.php new file mode 100644 index 0000000..04e960f --- /dev/null +++ b/src/ZipEntry.php @@ -0,0 +1,905 @@ + 'MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)', + self::MADE_BY_AMIGA => 'Amiga', + self::MADE_BY_OPEN_VMS => 'OpenVMS', + self::MADE_BY_UNIX => 'UNIX', + self::MADE_BY_VM_CMS => 'VM/CMS', + self::MADE_BY_ATARI => 'Atari ST', + self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.', + self::MADE_BY_MACINTOSH => 'Macintosh', + self::MADE_BY_Z_SYSTEM => 'Z-System', + self::MADE_BY_CP_M => 'CP/M', + self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS', + self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)', + self::MADE_BY_VSE => 'VSE', + self::MADE_BY_ACORN_RISC => 'Acorn Risc', + self::MADE_BY_VFAT => 'VFAT', + self::MADE_BY_ALTERNATE_MVS => 'alternate MVS', + self::MADE_BY_BEOS => 'BeOS', + self::MADE_BY_TANDEM => 'Tandem', + self::MADE_BY_OS_400 => 'OS/400', + self::MADE_BY_OS_X => 'OS X (Darwin)', + ); + + // constants version by extract + const EXTRACT_VERSION_10 = 10; + const EXTRACT_VERSION_11 = 11; + const EXTRACT_VERSION_20 = 20; +//1.0 - Default value +//1.1 - File is a volume label +//2.0 - File is a folder (directory) +//2.0 - File is compressed using Deflate compression +//2.0 - File is encrypted using traditional PKWARE encryption +//2.1 - File is compressed using Deflate64(tm) +//2.5 - File is compressed using PKWARE DCL Implode +//2.7 - File is a patch data set +//4.5 - File uses ZIP64 format extensions +//4.6 - File is compressed using BZIP2 compression* +//5.0 - File is encrypted using DES +//5.0 - File is encrypted using 3DES +//5.0 - File is encrypted using original RC2 encryption +//5.0 - File is encrypted using RC4 encryption +//5.1 - File is encrypted using AES encryption +//5.1 - File is encrypted using corrected RC2 encryption** +//5.2 - File is encrypted using corrected RC2-64 encryption** +//6.1 - File is encrypted using non-OAEP key wrapping*** +//6.2 - Central directory encryption +//6.3 - File is compressed using LZMA +//6.3 - File is compressed using PPMd+ +//6.3 - File is encrypted using Blowfish +//6.3 - File is encrypted using Twofish + + const FLAG_ENCRYPTION = 0; + const FLAG_DATA_DESCRIPTION = 3; + const FLAG_UTF8 = 11; + private static $valuesFlag = array( + self::FLAG_ENCRYPTION => 'encrypted file', // 1 << 0 + 1 => 'compression option', // 1 << 1 + 2 => 'compression option', // 1 << 2 + self::FLAG_DATA_DESCRIPTION => 'data descriptor', // 1 << 3 + 4 => 'enhanced deflation', // 1 << 4 + 5 => 'compressed patched data', // 1 << 5 + 6 => 'strong encryption', // 1 << 6 + 7 => 'unused', // 1 << 7 + 8 => 'unused', // 1 << 8 + 9 => 'unused', // 1 << 9 + 10 => 'unused', // 1 << 10 + self::FLAG_UTF8 => 'language encoding', // 1 << 11 + 12 => 'reserved', // 1 << 12 + 13 => 'mask header values', // 1 << 13 + 14 => 'reserved', // 1 << 14 + 15 => 'reserved', // 1 << 15 + ); + + // compression method constants + const COMPRESS_METHOD_STORED = 0; + const COMPRESS_METHOD_DEFLATED = 8; + const COMPRESS_METHOD_AES = 99; + + private static $valuesCompressionMethod = array( + self::COMPRESS_METHOD_STORED => 'no compression', + 1 => 'shrink', + 2 => 'reduce level 1', + 3 => 'reduce level 2', + 4 => 'reduce level 3', + 5 => 'reduce level 4', + 6 => 'implode', + 7 => 'reserved for Tokenizing compression algorithm', + self::COMPRESS_METHOD_DEFLATED => 'deflate', + 9 => 'deflate64', + 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', + 11 => 'reserved by PKWARE', + 12 => 'bzip2', + 13 => 'reserved by PKWARE', + 14 => 'LZMA (EFS)', + 15 => 'reserved by PKWARE', + 16 => 'reserved by PKWARE', + 17 => 'reserved by PKWARE', + 18 => 'IBM TERSE', + 19 => 'IBM LZ77 z Architecture (PFS)', + 97 => 'WavPack', + 98 => 'PPMd version I, Rev 1', + self::COMPRESS_METHOD_AES => 'AES Encryption', + ); + + const INTERNAL_ATTR_DEFAULT = 0; + const EXTERNAL_ATTR_DEFAULT = 0; + + /* + * Extra field header ID + */ + const EXTID_ZIP64 = 0x0001; // Zip64 + const EXTID_NTFS = 0x000a; // NTFS (for storing full file times information) + const EXTID_UNIX = 0x000d; // UNIX + const EXTID_EXTT = 0x5455; // Info-ZIP Extended Timestamp + const EXTID_UNICODE_FILENAME = 0x7075; // for Unicode filenames + const EXTID_UNICODE_ = 0x6375; // for Unicode file comments + const EXTID_STORING_STRINGS = 0x5A4C; // for storing strings code pages and Unicode filenames using custom Unicode implementation (see Unicode Support: Using Non-English Characters in Filenames, Comments and Passwords). + const EXTID_OFFSETS_COMPRESS_DATA = 0x5A4D; // for saving offsets array from seekable compressed data + const EXTID_AES_ENCRYPTION = 0x9901; // WinZip AES encryption (http://www.winzip.com/aes_info.htm) + + /** + * entry name + * @var string + */ + private $name; + /** + * version made by + * @var int + */ + private $versionMadeBy = self::MADE_BY_WINDOWS_NTFS; + /** + * version needed to extract + * @var int + */ + private $versionExtract = self::EXTRACT_VERSION_20; + /** + * general purpose bit flag + * @var int + */ + private $flag = 0; + /** + * compression method + * @var int + */ + private $compressionMethod = self::COMPRESS_METHOD_DEFLATED; + /** + * last mod file datetime + * @var int Unix timestamp + */ + private $lastModDateTime; + /** + * crc-32 + * @var int + */ + private $crc32; + /** + * compressed size + * @var int + */ + private $compressedSize; + /** + * uncompressed size + * @var int + */ + private $unCompressedSize; + /** + * disk number start + * @var int + */ + private $diskNumber = 0; + /** + * internal file attributes + * @var int + */ + private $internalAttributes = self::INTERNAL_ATTR_DEFAULT; + /** + * external file attributes + * @var int + */ + private $externalAttributes = self::EXTERNAL_ATTR_DEFAULT; + /** + * relative offset of local header + * @var int + */ + private $offsetOfLocal; + /** + * @var int + */ + private $offsetOfCentral; + + /** + * optional extra field data for entry + * + * @var string + */ + private $extraCentral = ""; + /** + * @var string + */ + private $extraLocal = ""; + /** + * optional comment string for entry + * + * @var string + */ + private $comment = ""; + + function __construct() + { + + } + + public function getLengthOfLocal() + { + return $this->getLengthLocalHeader() + $this->compressedSize + ($this->hasDataDescriptor() ? 12 : 0); + } + + public function getLengthLocalHeader() + { + return 30 + strlen($this->name) + strlen($this->extraLocal); + } + + public function getLengthOfCentral() + { + return 46 + strlen($this->name) + strlen($this->extraCentral) + strlen($this->comment); + } + + /** + * @param Buffer $buffer + * @throws ZipException + */ + public function readCentralHeader(Buffer $buffer) + { + $signature = $buffer->getUnsignedInt(); // after offset 4 + if ($signature !== ZipFile::SIGNATURE_CENTRAL_DIR) { + throw new ZipException("Can not read central directory. Bad signature: " . $signature); + } + $this->versionMadeBy = $buffer->getUnsignedShort(); // after offset 6 + $this->versionExtract = $buffer->getUnsignedShort(); // after offset 8 + $this->flag = $buffer->getUnsignedShort(); // after offset 10 + $this->compressionMethod = $buffer->getUnsignedShort(); // after offset 12 + $lastModTime = $buffer->getUnsignedShort(); // after offset 14 + $lastModDate = $buffer->getUnsignedShort(); // after offset 16 + $this->setLastModifyDosDatetime($lastModTime, $lastModDate); + $this->crc32 = $buffer->getUnsignedInt(); // after offset 20 + $this->compressedSize = $buffer->getUnsignedInt(); // after offset 24 + $this->unCompressedSize = $buffer->getUnsignedInt(); // after offset 28 + $fileNameLength = $buffer->getUnsignedShort(); // after offset 30 + $extraCentralLength = $buffer->getUnsignedShort(); // after offset 32 + $fileCommentLength = $buffer->getUnsignedShort(); // after offset 34 + $this->diskNumber = $buffer->getUnsignedShort(); // after offset 36 + $this->internalAttributes = $buffer->getUnsignedShort(); // after offset 38 + $this->externalAttributes = $buffer->getUnsignedInt(); // after offset 42 + $this->offsetOfLocal = $buffer->getUnsignedInt(); // after offset 46 + $this->name = $buffer->getString($fileNameLength); + $this->setExtra($buffer->getString($extraCentralLength)); + $this->comment = $buffer->getString($fileCommentLength); + + $currentPos = $buffer->position(); + $buffer->setPosition($this->offsetOfLocal + 28); + $extraLocalLength = $buffer->getUnsignedShort(); + $buffer->skip($fileNameLength); + $this->extraLocal = $buffer->getString($extraLocalLength); + $buffer->setPosition($currentPos); + } + + /** + * Sets the optional extra field data for the entry. + * + * @param string $extra the extra field data bytes + * @throws ZipException + */ + private function setExtra($extra) + { + if (!empty($extra)) { + $len = strlen($extra); + if ($len > 0xFFFF) { + throw new ZipException("invalid extra field length"); + } + $buffer = new StringBuffer($extra); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + // extra fields are in "HeaderID(2)DataSize(2)Data... format + while ($buffer->position() + 4 < $len) { + $tag = $buffer->getUnsignedShort(); + $sz = $buffer->getUnsignedShort(); + if ($buffer->position() + $sz > $len) // invalid data + break; + switch ($tag) { + case self::EXTID_ZIP64: + // not support zip64 + break; + case self::EXTID_NTFS: + $buffer->skip(4); // reserved 4 bytes + if ($buffer->getUnsignedShort() != 0x0001 || $buffer->getUnsignedShort() != 24) + break; +// $mtime = winTimeToFileTime($buffer->getLong()); +// $atime = winTimeToFileTime($buffer->getLong()); +// $ctime = winTimeToFileTime($buffer->getLong()); + break; + case self::EXTID_EXTT: + $flag = $buffer->getUnsignedByte(); + $sz0 = 1; + // The CEN-header extra field contains the modification + // time only, or no timestamp at all. 'sz' is used to + // flag its presence or absence. But if mtime is present + // in LOC it must be present in CEN as well. + if (($flag & 0x1) != 0 && ($sz0 + 4) <= $sz) { + $mtime = $buffer->getUnsignedInt(); + $sz0 += 4; + } + if (($flag & 0x2) != 0 && ($sz0 + 4) <= $sz) { + $atime = $buffer->getUnsignedInt(); + $sz0 += 4; + } + if (($flag & 0x4) != 0 && ($sz0 + 4) <= $sz) { + $ctime = $buffer->getUnsignedInt(); + $sz0 += 4; + } + break; + default: + } + } + } + $this->extraCentral = $extra; + } + + /** + * @return Buffer + */ + public function writeLocalHeader() + { + $buffer = new StringBuffer(); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + $buffer->insertInt(ZipFile::SIGNATURE_LOCAL_HEADER); + $buffer->insertShort($this->versionExtract); + $buffer->insertShort($this->flag); + $buffer->insertShort($this->compressionMethod); + $buffer->insertShort($this->getLastModifyDosTime()); + $buffer->insertShort($this->getLastModifyDosDate()); + if ($this->hasDataDescriptor()) { + $buffer->insertInt(0); + $buffer->insertInt(0); + $buffer->insertInt(0); + } else { + $buffer->insertInt($this->crc32); + $buffer->insertInt($this->compressedSize); + $buffer->insertInt($this->unCompressedSize); + } + $buffer->insertShort(strlen($this->name)); + $buffer->insertShort(strlen($this->extraLocal)); // offset 30 + $buffer->insertString($this->name); + $buffer->insertString($this->extraLocal); + return $buffer; + } + + /** + * @param int $bit + * @return bool + */ + public function setFlagBit($bit) + { + if ($bit < 0 || $bit > 15) { + return false; + } + $this->flag |= 1 << $bit; + return true; + } + + /** + * @param int $bit + * @return bool + */ + public function testFlagBit($bit) + { + return (($this->flag & (1 << $bit)) !== 0); + } + + /** + * @return bool + */ + public function hasDataDescriptor() + { + return $this->testFlagBit(self::FLAG_DATA_DESCRIPTION); + } + + /** + * @return bool + */ + public function isEncrypted() + { + return $this->testFlagBit(self::FLAG_ENCRYPTION); + } + + public function writeDataDescriptor() + { + $buffer = new StringBuffer(); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + $buffer->insertInt($this->crc32); + $buffer->insertInt($this->compressedSize); + $buffer->insertInt($this->unCompressedSize); + return $buffer; + } + + /** + * @return Buffer + * @throws ZipException + */ + public function writeCentralHeader() + { + $buffer = new StringBuffer(); + $buffer->setOrder(Buffer::LITTLE_ENDIAN); + + $buffer->insertInt(ZipFile::SIGNATURE_CENTRAL_DIR); + $buffer->insertShort($this->versionMadeBy); + $buffer->insertShort($this->versionExtract); + $buffer->insertShort($this->flag); + $buffer->insertShort($this->compressionMethod); + $buffer->insertShort($this->getLastModifyDosTime()); + $buffer->insertShort($this->getLastModifyDosDate()); + $buffer->insertInt($this->crc32); + $buffer->insertInt($this->compressedSize); + $buffer->insertInt($this->unCompressedSize); + $buffer->insertShort(strlen($this->name)); + $buffer->insertShort(strlen($this->extraCentral)); + $buffer->insertShort(strlen($this->comment)); + $buffer->insertShort($this->diskNumber); + $buffer->insertShort($this->internalAttributes); + $buffer->insertInt($this->externalAttributes); + $buffer->insertInt($this->offsetOfLocal); + $buffer->insertString($this->name); + $buffer->insertString($this->extraCentral); + $buffer->insertString($this->comment); + return $buffer; + } + + /** + * @return bool + */ + public function isDirectory() + { + return $this->name[strlen($this->name) - 1] === "/"; + } + + /** + * @return array + */ + public static function getValuesMadeBy() + { + return self::$valuesMadeBy; + } + + /** + * @param array $valuesMadeBy + */ + public static function setValuesMadeBy($valuesMadeBy) + { + self::$valuesMadeBy = $valuesMadeBy; + } + + /** + * @return array + */ + public static function getValuesFlag() + { + return self::$valuesFlag; + } + + /** + * @param array $valuesFlag + */ + public static function setValuesFlag($valuesFlag) + { + self::$valuesFlag = $valuesFlag; + } + + /** + * @return array + */ + public static function getValuesCompressionMethod() + { + return self::$valuesCompressionMethod; + } + + /** + * @param array $valuesCompressionMethod + */ + public static function setValuesCompressionMethod($valuesCompressionMethod) + { + self::$valuesCompressionMethod = $valuesCompressionMethod; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * @throws ZipException + */ + public function setName($name) + { + if (strlen($name) > 0xFFFF) { + throw new ZipException("entry name too long"); + } + $this->name = $name; + $encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true); + if ($encoding === 'UTF-8') { + $this->setFlagBit(self::FLAG_UTF8); + } + } + + /** + * @return int + */ + public function getVersionMadeBy() + { + return $this->versionMadeBy; + } + + /** + * @param int $versionMadeBy + */ + public function setVersionMadeBy($versionMadeBy) + { + $this->versionMadeBy = $versionMadeBy; + } + + /** + * @return int + */ + public function getVersionExtract() + { + return $this->versionExtract; + } + + /** + * @param int $versionExtract + */ + public function setVersionExtract($versionExtract) + { + $this->versionExtract = $versionExtract; + } + + /** + * @return int + */ + public function getFlag() + { + return $this->flag; + } + + /** + * @param int $flag + */ + public function setFlag($flag) + { + $this->flag = $flag; + } + + /** + * @return int + */ + public function getCompressionMethod() + { + return $this->compressionMethod; + } + + /** + * @param int $compressionMethod + * @throws ZipException + */ + public function setCompressionMethod($compressionMethod) + { + if (!isset(self::$valuesCompressionMethod[$compressionMethod])) { + throw new ZipException("invalid compression method " . $compressionMethod); + } + $this->compressionMethod = $compressionMethod; + } + + /** + * @return int + */ + public function getLastModDateTime() + { + return $this->lastModDateTime; + } + + /** + * @param int $lastModDateTime + */ + public function setLastModDateTime($lastModDateTime) + { + $this->lastModDateTime = $lastModDateTime; + } + + /** + * @return int + */ + public function getCrc32() + { + return $this->crc32; + } + + /** + * @param int $crc32 + * @throws ZipException + */ + public function setCrc32($crc32) + { + if ($crc32 < 0 || $crc32 > 0xFFFFFFFF) { + throw new ZipException("invalid entry crc-32"); + } + $this->crc32 = $crc32; + } + + /** + * @return int + */ + public function getCompressedSize() + { + return $this->compressedSize; + } + + /** + * @param int $compressedSize + */ + public function setCompressedSize($compressedSize) + { + $this->compressedSize = $compressedSize; + } + + /** + * @return int + */ + public function getUnCompressedSize() + { + return $this->unCompressedSize; + } + + /** + * @param int $unCompressedSize + * @throws ZipException + */ + public function setUnCompressedSize($unCompressedSize) + { + if ($unCompressedSize < 0 || $unCompressedSize > 0xFFFFFFFF) { + throw new ZipException("invalid entry size"); + } + $this->unCompressedSize = $unCompressedSize; + } + + /** + * @return int + */ + public function getDiskNumber() + { + return $this->diskNumber; + } + + /** + * @param int $diskNumber + */ + public function setDiskNumber($diskNumber) + { + $this->diskNumber = $diskNumber; + } + + /** + * @return int + */ + public function getInternalAttributes() + { + return $this->internalAttributes; + } + + /** + * @param int $internalAttributes + */ + public function setInternalAttributes($internalAttributes) + { + $this->internalAttributes = $internalAttributes; + } + + /** + * @return int + */ + public function getExternalAttributes() + { + return $this->externalAttributes; + } + + /** + * @param int $externalAttributes + */ + public function setExternalAttributes($externalAttributes) + { + $this->externalAttributes = $externalAttributes; + } + + /** + * @return int + */ + public function getOffsetOfLocal() + { + return $this->offsetOfLocal; + } + + /** + * @param int $offsetOfLocal + */ + public function setOffsetOfLocal($offsetOfLocal) + { + $this->offsetOfLocal = $offsetOfLocal; + } + + /** + * @return int + */ + public function getOffsetOfCentral() + { + return $this->offsetOfCentral; + } + + /** + * @param int $offsetOfCentral + */ + public function setOffsetOfCentral($offsetOfCentral) + { + $this->offsetOfCentral = $offsetOfCentral; + } + + /** + * @return string + */ + public function getExtraCentral() + { + return $this->extraCentral; + } + + /** + * @param string $extra + * @throws ZipException + */ + public function setExtraCentral($extra) + { + if ($extra !== null && strlen($extra) > 0xFFFF) { + throw new ZipException("invalid extra field length"); + } + $this->extraCentral = $extra; + } + + /** + * @param string $extra + * @throws ZipException + */ + public function setExtraLocal($extra) + { + if ($extra !== null && strlen($extra) > 0xFFFF) { + throw new ZipException("invalid extra field length"); + } + $this->extraLocal = $extra; + } + + /** + * @return string + */ + public function getExtraLocal() + { + return $this->extraLocal; + } + + /** + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * @param string $comment + */ + public function setComment($comment) + { + $this->comment = $comment; + } + + /** + * @param int $lastModTime + * @param int $lastModDate + */ + private function setLastModifyDosDatetime($lastModTime, $lastModDate) + { + $hour = ($lastModTime & 0xF800) >> 11; + $minute = ($lastModTime & 0x07E0) >> 5; + $seconds = ($lastModTime & 0x001F) * 2; + + $year = (($lastModDate & 0xFE00) >> 9) + 1980; + $month = ($lastModDate & 0x01E0) >> 5; + $day = $lastModDate & 0x001F; + + // ----- Get UNIX date format + $this->lastModDateTime = mktime($hour, $minute, $seconds, $month, $day, $year); + } + + public function getLastModifyDosTime() + { + $date = getdate($this->lastModDateTime); + return ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2; + } + + public function getLastModifyDosDate() + { + $date = getdate($this->lastModDateTime); + return (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday']; + } + + public function versionMadeToString() + { + if (isset(self::$valuesMadeBy[$this->versionMadeBy])) { + return self::$valuesMadeBy[$this->versionMadeBy]; + } else return "unknown"; + } + + public function compressionMethodToString() + { + if (isset(self::$valuesCompressionMethod[$this->compressionMethod])) { + return self::$valuesCompressionMethod[$this->compressionMethod]; + } else return "unknown"; + } + + public function flagToString() + { + $return = array(); + foreach (self::$valuesFlag AS $bit => $value) { + if ($this->testFlagBit($bit)) { + $return[] = $value; + } + } + if (!empty($return)) { + return implode(', ', $return); + } else if ($this->flag === 0) { + return "default"; + } + return "unknown"; + } + + function __toString() + { + return __CLASS__ . '{' . + 'name="' . $this->name . '"' . + ', versionMadeBy={' . $this->versionMadeBy . ' => "' . $this->versionMadeToString() . '"}' . + ', versionExtract="' . $this->versionExtract . '"' . + ', flag={' . $this->flag . ' => ' . $this->flagToString() . '}' . + ', compressionMethod={' . $this->compressionMethod . ' => ' . $this->compressionMethodToString() . '}' . + ', lastModify=' . date("Y-m-d H:i:s", $this->lastModDateTime) . + ', crc32=0x' . dechex($this->crc32) . + ', compressedSize=' . ZipUtils::humanSize($this->compressedSize) . + ', unCompressedSize=' . ZipUtils::humanSize($this->unCompressedSize) . + ', diskNumber=' . $this->diskNumber . + ', internalAttributes=' . $this->internalAttributes . + ', externalAttributes=' . $this->externalAttributes . + ', offsetOfLocal=' . $this->offsetOfLocal . + ', offsetOfCentral=' . $this->offsetOfCentral . + ', extraCentral="' . $this->extraCentral . '"' . + ', extraLocal="' . $this->extraLocal . '"' . + ', comment="' . $this->comment . '"' . + '}'; + } +} \ No newline at end of file diff --git a/src/ZipException.php b/src/ZipException.php new file mode 100644 index 0000000..8beaacb --- /dev/null +++ b/src/ZipException.php @@ -0,0 +1,7 @@ +filename = null; + $this->zipEntries = array(); + $this->zipEntriesIndex = array(); + $this->zipComment = ""; + $this->offsetCentralDirectory = 0; + $this->sizeCentralDirectory = 0; + + $this->buffer = new MemoryResourceBuffer(); + $this->buffer->setOrder(Buffer::LITTLE_ENDIAN); + $this->buffer->insertInt(self::SIGNATURE_END_CENTRAL_DIR); + $this->buffer->insertString(str_repeat("\0", 18)); + } + + /** + * Open exists zip archive + * + * @param string $filename + * @throws ZipException + */ + public function open($filename) + { + if (!file_exists($filename)) { + throw new ZipException("Can not open file"); + } + $this->filename = $filename; + $this->openFromString(file_get_contents($this->filename)); + } + + public function openFromString($string) + { + $this->zipEntries = null; + $this->zipEntriesIndex = null; + $this->zipComment = ""; + $this->offsetCentralDirectory = null; + $this->sizeCentralDirectory = 0; + $this->password = null; + + $this->buffer = new StringBuffer($string); + $this->buffer->setOrder(Buffer::LITTLE_ENDIAN); + + $this->findAndReadEndCentralDirectory(); + } + + /** + * Set password + * + * @param string $password + */ + public function setPassword($password) + { + $this->password = $password; + } + + /** + * Find end central catalog + * + * @throws BufferException + * @throws ZipException + */ + private function findAndReadEndCentralDirectory() + { + if ($this->buffer->size() < 26) { + return; + } + $this->buffer->setPosition($this->buffer->size() - 22); + + $endOfCentralDirSignature = $this->buffer->getUnsignedInt(); + if ($endOfCentralDirSignature === self::SIGNATURE_END_CENTRAL_DIR) { + $this->readEndCentralDirectory(); + } else { + $maximumSize = 65557; + if ($this->buffer->size() < $maximumSize) { + $maximumSize = $this->buffer->size(); + } + $this->buffer->skip(-$maximumSize); + $bytes = 0x00000000; + while ($this->buffer->hasRemaining()) { + $byte = $this->buffer->getUnsignedByte(); + $bytes = (($bytes & 0xFFFFFF) << 8) | $byte; + + if ($bytes === 0x504b0506) { + $this->readEndCentralDirectory(); + return; + } + } + throw new ZipException("Unable to find End of Central Dir Record signature"); + } + } + + /** + * Read end central catalog + * + * @throws BufferException + * @throws ZipException + */ + private function readEndCentralDirectory() + { + $this->buffer->skip(4); // number of this disk AND number of the disk with the start of the central directory + $countFiles = $this->buffer->getUnsignedShort(); + $this->buffer->skip(2); // total number of entries in the central directory + $this->sizeCentralDirectory = $this->buffer->getUnsignedInt(); + $this->offsetCentralDirectory = $this->buffer->getUnsignedInt(); + $zipCommentLength = $this->buffer->getUnsignedShort(); + $this->zipComment = $this->buffer->getString($zipCommentLength); + + $this->buffer->setPosition($this->offsetCentralDirectory); + + $this->zipEntries = array(); + $this->zipEntriesIndex = array(); + + for ($i = 0; $i < $countFiles; $i++) { + $offsetOfCentral = $this->buffer->position() - $this->offsetCentralDirectory; + + $zipEntry = new ZipEntry(); + $zipEntry->readCentralHeader($this->buffer); + $zipEntry->setOffsetOfCentral($offsetOfCentral); + + $this->zipEntries[$i] = $zipEntry; + $this->zipEntriesIndex[$zipEntry->getName()] = $i; + } + } + + /** + * @return int + */ + public function getCountFiles() + { + return $this->zipEntries === null ? 0 : sizeof($this->zipEntries); + } + + /** + * Add empty directory in zip archive + * + * @param string $dirName + * @return bool + * @throws ZipException + */ + public function addEmptyDir($dirName) + { + if ($dirName === null) { + throw new ZipException("dirName null"); + } + $dirName = rtrim($dirName, '/') . '/'; + if (isset($this->zipEntriesIndex[$dirName])) { + return true; + } + $zipEntry = new ZipEntry(); + $zipEntry->setName($dirName); + $zipEntry->setCompressionMethod(0); + $zipEntry->setLastModDateTime(time()); + $zipEntry->setCrc32(0); + $zipEntry->setCompressedSize(0); + $zipEntry->setUnCompressedSize(0); + $zipEntry->setOffsetOfLocal($this->offsetCentralDirectory); + + $this->buffer->setPosition($zipEntry->getOffsetOfLocal()); + $bufferLocal = $zipEntry->writeLocalHeader(); + $this->buffer->insert($bufferLocal); + $this->offsetCentralDirectory += $bufferLocal->size(); + + $zipEntry->setOffsetOfCentral($this->sizeCentralDirectory); + $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); + $bufferCentral = $zipEntry->writeCentralHeader(); + $this->buffer->insert($bufferCentral); + $this->sizeCentralDirectory += $bufferCentral->size(); + + $this->zipEntries[] = $zipEntry; + end($this->zipEntries); + $this->zipEntriesIndex[$zipEntry->getName()] = key($this->zipEntries); + + $size = $this->getCountFiles(); + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(4); + $this->buffer->putShort($size); + $this->buffer->putShort($size); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + return true; + } + + /** + * @param string $inDirectory + * @param string|null $addPath + * @param array $ignoreFiles + * @return bool + * @throws ZipException + */ + public function addDir($inDirectory, $addPath = null, array $ignoreFiles = array()) + { + if ($inDirectory === null) { + throw new ZipException("dirName null"); + } + if (!file_exists($inDirectory)) { + throw new ZipException("directory not found"); + } + if (!is_dir($inDirectory)) { + throw new ZipException("input directory is not directory"); + } + if ($addPath !== null && is_string($addPath) && !empty($addPath)) { + $addPath = rtrim($addPath, '/'); + } else { + $addPath = ""; + } + $inDirectory = rtrim($inDirectory, '/'); + + $iterator = new FilterFileIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($inDirectory)), $ignoreFiles); + $files = iterator_to_array($iterator, false); + + $count = $this->getCountFiles(); + /** + * @var \SplFileInfo $file + */ + foreach ($files as $file) { + if ($file->getFilename() === '.') { + $filename = dirname(str_replace($inDirectory, $addPath, $file)); + $this->isEmptyDir($file) && $this->addEmptyDir($filename); + } else if ($file->isFile()) { + $filename = str_replace($inDirectory, $addPath, $file); + $this->addFile($file, $filename); + } + } + return $this->getCountFiles() > $count; + } + + public function addGlob($pattern, $removePath = null, $addPath = null, $recursive = true) + { + if ($pattern === null) { + throw new ZipException("pattern null"); + } + $glob = $this->globFileSearch($pattern, GLOB_BRACE, $recursive); + if ($glob === FALSE || empty($glob)) { + return false; + } + if (!empty($addPath) && is_string($addPath)) { + $addPath = rtrim($addPath, '/'); + } else { + $addPath = ""; + } + if (!empty($removePath) && is_string($removePath)) { + $removePath = rtrim($removePath, '/'); + } else { + $removePath = ""; + } + + $count = $this->getCountFiles(); + /** + * @var string $file + */ + foreach ($glob as $file) { + if (is_dir($file)) { + $filename = str_replace($addPath, $removePath, $file); + $this->isEmptyDir($file) && $this->addEmptyDir($filename); + } else if (is_file($file)) { + $filename = str_replace($removePath, $addPath, $file); + $this->addFile($file, $filename); + } + } + return $this->getCountFiles() > $count; + } + + public function addPattern($pattern, $inDirectory, $addPath = null, $recursive = true) + { + if ($pattern === null) { + throw new ZipException("pattern null"); + } + $files = $this->regexFileSearch($inDirectory, $pattern, $recursive); + if ($files === FALSE || empty($files)) { + return false; + } + if (!empty($addPath) && is_string($addPath)) { + $addPath = rtrim($addPath, '/'); + } else { + $addPath = ""; + } + $inDirectory = rtrim($inDirectory, '/'); + + $count = $this->getCountFiles(); + /** + * @var string $file + */ + foreach ($files as $file) { + if (is_dir($file)) { + $filename = str_replace($addPath, $inDirectory, $file); + $this->isEmptyDir($file) && $this->addEmptyDir($filename); + } else if (is_file($file)) { + $filename = str_replace($inDirectory, $addPath, $file); + $this->addFile($file, $filename); + } + } + return $this->getCountFiles() > $count; + } + + private function globFileSearch($pattern, $flags = 0, $recursive = true) + { + $files = glob($pattern, $flags); + if (!$recursive) return $files; + foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $files = array_merge($files, $this->globFileSearch($dir . '/' . basename($pattern), $flags, $recursive)); + } + return $files; + } + + private function regexFileSearch($folder, $pattern, $recursive = true) + { + $dir = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder); + $ite = $recursive ? new \RecursiveIteratorIterator($dir) : new \IteratorIterator($dir); + $files = new \RegexIterator($ite, $pattern, \RegexIterator::GET_MATCH); + $fileList = array(); + foreach ($files as $file) { + $fileList = array_merge($fileList, $file); + } + return $fileList; + } + + private function isEmptyDir($dir) + { + if (!is_readable($dir)) return false; + return (count(scandir($dir)) == 2); + } + + /** + * Add file in zip archive + * + * @param string $filename + * @param string|null $localName + * @param int|null $compressionMethod + * @throws ZipException + */ + public function addFile($filename, $localName = NULL, $compressionMethod = null) + { + if ($filename === null) { + throw new ZipException("filename null"); + } + if (!file_exists($filename)) { + throw new ZipException("file not found"); + } + if (!is_file($filename)) { + throw new ZipException("input filename is not file"); + } + if ($localName === null) { + $localName = basename($filename); + } + $this->addFromString($localName, file_get_contents($filename), $compressionMethod); + } + + /** + * @param string $localName + * @param string $contents + * @param int|null $compressionMethod + * @throws ZipException + */ + public function addFromString($localName, $contents, $compressionMethod = null) + { + if ($localName === null || !is_string($localName) || strlen($localName) === 0) { + throw new ZipException("local name empty"); + } + if ($contents === null) { + throw new ZipException("contents null"); + } + $unCompressedSize = strlen($contents); + $compress = null; + if ($compressionMethod === null) { + if ($unCompressedSize === 0) { + $compressionMethod = ZipEntry::COMPRESS_METHOD_STORED; + } else { + $compressionMethod = ZipEntry::COMPRESS_METHOD_DEFLATED; + } + } + switch ($compressionMethod) { + case ZipEntry::COMPRESS_METHOD_STORED: + $compress = $contents; + break; + case ZipEntry::COMPRESS_METHOD_DEFLATED: + $compress = gzdeflate($contents); + break; + default: + throw new ZipException("Compression method not support"); + } + $crc32 = sprintf('%u', crc32($contents)); + $compressedSize = strlen($compress); + + if (isset($this->zipEntriesIndex[$localName])) { + /** + * @var int $index + */ + $index = $this->zipEntriesIndex[$localName]; + $zipEntry = &$this->zipEntries[$index]; + + $oldCompressedSize = $zipEntry->getCompressedSize(); + + $zipEntry->setCompressionMethod($compressionMethod); + $zipEntry->setLastModDateTime(time()); + $zipEntry->setCompressedSize($compressedSize); + $zipEntry->setUnCompressedSize($unCompressedSize); + $zipEntry->setCrc32($crc32); + + $this->buffer->setPosition($zipEntry->getOffsetOfLocal() + 8); + $this->buffer->putShort($zipEntry->getCompressionMethod()); + $this->buffer->putShort($zipEntry->getLastModifyDosTime()); + $this->buffer->putShort($zipEntry->getLastModifyDosDate()); + if ($zipEntry->hasDataDescriptor()) { + $this->buffer->skip(12); + } else { + $this->buffer->putInt($zipEntry->getCrc32()); + $this->buffer->putInt($zipEntry->getCompressedSize()); + $this->buffer->putInt($zipEntry->getUnCompressedSize()); + } + $this->buffer->skip(4 + strlen($zipEntry->getName()) + strlen($zipEntry->getExtraLocal())); + $this->buffer->replaceString($compress, $oldCompressedSize); + + if ($zipEntry->hasDataDescriptor()) { + $this->buffer->put($zipEntry->writeDataDescriptor()); + } + + $diff = $oldCompressedSize - $zipEntry->getCompressedSize(); + if ($diff !== 0) { + $this->offsetCentralDirectory -= $diff; + } + $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 10); + $this->buffer->putShort($zipEntry->getCompressionMethod()); + $this->buffer->putShort($zipEntry->getLastModifyDosTime()); + $this->buffer->putShort($zipEntry->getLastModifyDosDate()); + $this->buffer->putInt($zipEntry->getCrc32()); + $this->buffer->putInt($zipEntry->getCompressedSize()); + $this->buffer->putInt($zipEntry->getUnCompressedSize()); + + if ($diff !== 0) { + $this->buffer->skip(18 + strlen($zipEntry->getName()) + strlen($zipEntry->getExtraCentral()) + strlen($zipEntry->getComment())); + + $size = $this->getCountFiles(); + /** + * @var ZipEntry $entry + */ + for ($i = $index + 1; $i < $size; $i++) { + $zipEntry = &$this->zipEntries[$i]; + + $zipEntry->setOffsetOfLocal($zipEntry->getOffsetOfLocal() - $diff); + $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 42); +// $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); +// $sig = $this->buffer->getUnsignedInt(); +// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { +// $this->buffer->skip(-4); +// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); +// } +// $this->buffer->skip(38); + $this->buffer->putInt($zipEntry->getOffsetOfLocal()); + } + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(8); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + } + } else { + $zipEntry = new ZipEntry(); +// if ($flagBit > 0) $zipEntry->setFlagBit($flagBit); + $zipEntry->setName($localName); + $zipEntry->setCompressionMethod($compressionMethod); + $zipEntry->setLastModDateTime(time()); + $zipEntry->setCrc32($crc32); + $zipEntry->setCompressedSize($compressedSize); + $zipEntry->setUnCompressedSize($unCompressedSize); + $zipEntry->setOffsetOfLocal($this->offsetCentralDirectory); + + $bufferLocal = $zipEntry->writeLocalHeader(); + $bufferLocal->insertString($compress); + if ($zipEntry->hasDataDescriptor()) { + $bufferLocal->insert($zipEntry->writeDataDescriptor()); + } + + $this->buffer->setPosition($zipEntry->getOffsetOfLocal()); + $this->buffer->insert($bufferLocal); + $this->offsetCentralDirectory += $bufferLocal->size(); + + $zipEntry->setOffsetOfCentral($this->sizeCentralDirectory); + $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); + $bufferCentral = $zipEntry->writeCentralHeader(); + $this->buffer->insert($bufferCentral); + $this->sizeCentralDirectory += $bufferCentral->size(); + + $this->zipEntries[] = $zipEntry; + end($this->zipEntries); + $this->zipEntriesIndex[$zipEntry->getName()] = key($this->zipEntries); + + $size = $this->getCountFiles(); + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(4); + $this->buffer->putShort($size); + $this->buffer->putShort($size); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + } + } + + /** + * Update timestamp archive for all files + * + * @param int|null $timestamp + * @throws BufferException + */ + public function updateTimestamp($timestamp = null) + { + if ($timestamp === null || !is_int($timestamp)) { + $timestamp = time(); + } + foreach ($this->zipEntries AS $entry) { + $entry->setLastModDateTime($timestamp); + $this->buffer->setPosition($entry->getOffsetOfLocal() + 10); + $this->buffer->putShort($entry->getLastModifyDosTime()); + $this->buffer->putShort($entry->getLastModifyDosDate()); + + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 12); + $this->buffer->putShort($entry->getLastModifyDosTime()); + $this->buffer->putShort($entry->getLastModifyDosDate()); + } + } + + public function deleteGlob($pattern) + { + if ($pattern === null) { + throw new ZipException("pattern null"); + } + $pattern = '~' . $this->convertGlobToRegEx($pattern) . '~si'; + return $this->deletePattern($pattern); + } + + public function deletePattern($pattern) + { + if ($pattern === null) { + throw new ZipException("pattern null"); + } + $offsetLocal = 0; + $offsetCentral = 0; + $modify = false; + foreach ($this->zipEntries AS $index => &$entry) { + if (preg_match($pattern, $entry->getName())) { + $this->buffer->setPosition($entry->getOffsetOfLocal() - $offsetLocal); + $lengthLocal = $entry->getLengthOfLocal(); + $this->buffer->remove($lengthLocal); + $offsetLocal += $lengthLocal; + + $this->offsetCentralDirectory -= $lengthLocal; + + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() - $offsetCentral); + $lengthCentral = $entry->getLengthOfCentral(); + $this->buffer->remove($lengthCentral); + $offsetCentral += $lengthCentral; + + $this->sizeCentralDirectory -= $lengthCentral; + + unset($this->zipEntries[$index], $this->zipEntriesIndex[$entry->getName()]); + $modify = true; + continue; + } + if ($modify) { + $entry->setOffsetOfLocal($entry->getOffsetOfLocal() - $offsetLocal); + $entry->setOffsetOfCentral($entry->getOffsetOfCentral() - $offsetCentral); + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 42); + $this->buffer->putInt($entry->getOffsetOfLocal()); + } + } + if ($modify) { + $size = $this->getCountFiles(); + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(4); + $this->buffer->putShort($size); + $this->buffer->putShort($size); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + return true; + } + return false; + } + + /** + * @param int $index + * @return bool + * @throws ZipException + */ + public function deleteIndex($index) + { + if ($index === null || !is_numeric($index)) { + throw new ZipException("index no numeric"); + } + if (!isset($this->zipEntries[$index])) { + return false; + } + + $entry = $this->zipEntries[$index]; + + $offsetCentral = $entry->getOffsetOfCentral(); + $lengthCentral = $entry->getLengthOfCentral(); + + $offsetLocal = $entry->getOffsetOfLocal(); + $lengthLocal = $entry->getLengthOfLocal(); + + unset( + $this->zipEntries[$index], + $this->zipEntriesIndex[$entry->getName()] + ); + $this->zipEntries = array_values($this->zipEntries); + $this->zipEntriesIndex = array_flip(array_keys($this->zipEntriesIndex)); + + $size = $this->getCountFiles(); + + $this->buffer->setPosition($this->offsetCentralDirectory + $offsetCentral); + $this->buffer->remove($lengthCentral); + + $this->buffer->setPosition($offsetLocal); + $this->buffer->remove($lengthLocal); + + $this->offsetCentralDirectory -= $lengthLocal; + $this->sizeCentralDirectory -= $lengthCentral; + + /** + * @var ZipEntry $entry + */ + for ($i = $index; $i < $size; $i++) { + $entry = &$this->zipEntries[$i]; + + $entry->setOffsetOfLocal($entry->getOffsetOfLocal() - $lengthLocal); +// $this->buffer->setPosition($entry->getOffsetOfLocal()); +// $sig = $this->buffer->getUnsignedInt(); +// if ($sig !== self::SIGNATURE_LOCAL_HEADER) { +// throw new ZipException("Signature local header corrupt"); +// } + $entry->setOffsetOfCentral($entry->getOffsetOfCentral() - $lengthCentral); + + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 42); +// $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral()); +// $sig = $this->buffer->getUnsignedInt(); +// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { +// $this->buffer->skip(-4); +// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); +// } +// $this->buffer->skip(38); + $this->buffer->putInt($entry->getOffsetOfLocal()); + } + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(4); + $this->buffer->putShort($size); + $this->buffer->putShort($size); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + return true; + } + + public function deleteAll() + { + $this->zipEntries = array(); + $this->zipEntriesIndex = array(); + $this->offsetCentralDirectory = 0; + $this->sizeCentralDirectory = 0; + + $this->buffer->truncate(); + $this->buffer->insertInt(self::SIGNATURE_END_CENTRAL_DIR); + $this->buffer->insertString(str_repeat("\0", 18)); + } + + /** + * @param $name + * @return bool + * @throws ZipException + */ + public function deleteName($name) + { + if (empty($name)) { + throw new ZipException("name is empty"); + } + if (!isset($this->zipEntriesIndex[$name])) { + return false; + } + $index = $this->zipEntriesIndex[$name]; + return $this->deleteIndex($index); + } + + /** + * @param string $destination + * @param array $entries + * @return bool + * @throws ZipException + */ + public function extractTo($destination, array $entries = null) + { + if ($this->zipEntries === NULL) { + throw new ZipException("zip entries not initial"); + } + if (!file_exists($destination)) { + throw new ZipException("Destination " . $destination . " not found"); + } + if (!is_dir($destination)) { + throw new ZipException("Destination is not directory"); + } + if (!is_writable($destination)) { + throw new ZipException("Destination is not writable directory"); + } + + /** + * @var ZipEntry[] $zipEntries + */ + if ($entries !== null && is_array($entries) && !empty($entries)) { + $flipEntries = array_flip($entries); + $zipEntries = array_filter($this->zipEntries, function ($zipEntry) use ($flipEntries) { + /** + * @var ZipEntry $zipEntry + */ + return isset($flipEntries[$zipEntry->getName()]); + }); + } else { + $zipEntries = $this->zipEntries; + } + + $extract = 0; + foreach ($zipEntries AS $entry) { + $file = $destination . '/' . $entry->getName(); + $dir = dirname($file); + if (!file_exists($dir)) { + if (!mkdir($dir, 0755, true)) { + throw new ZipException("Can not create dir " . $dir); + } + chmod($dir, 0755); + } + if ($entry->isDirectory()) { + continue; + } + if (file_put_contents($file, $this->getEntryBytes($entry)) === FALSE) { + return false; + } + touch($file, $entry->getLastModDateTime()); + $extract++; + } + return $extract > 0; + } + + /** + * @param ZipEntry $entry + * @return string + * @throws BufferException + * @throws ZipException + */ + private function getEntryBytes(ZipEntry $entry) + { + $this->buffer->setPosition($entry->getOffsetOfLocal() + $entry->getLengthLocalHeader()); +// $this->buffer->setPosition($entry->getOffsetOfLocal()); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_LOCAL_HEADER) { +// throw new ZipException("Can not read entry " . $entry->getName()); +// } +// $this->buffer->skip($entry->getLengthLocalHeader() - 4); + + $string = $this->buffer->getString($entry->getCompressedSize()); + + if ($entry->isEncrypted()) { + if (empty($this->password)) { + throw new ZipException("need password archive"); + } + + $pwdKeys = self::$initPwdKeys; + + $bufPass = new StringBuffer($this->password); + while ($bufPass->hasRemaining()) { + $byte = $bufPass->getUnsignedByte(); + $pwdKeys = ZipUtils::updateKeys($byte, $pwdKeys); + } + unset($bufPass); + + $keys = $pwdKeys; + + $strBuffer = new StringBuffer($string); + for ($i = 0; $i < ZipUtils::DECRYPT_HEADER_SIZE; $i++) { + $result = $strBuffer->getUnsignedByte(); + $lastValue = $result ^ ZipUtils::decryptByte($keys[2]); + $keys = ZipUtils::updateKeys($lastValue, $keys); + } + + $string = ""; + while ($strBuffer->hasRemaining()) { + $result = $strBuffer->getUnsignedByte(); + $result = ($result ^ ZipUtils::decryptByte($keys[2])) & 0xff; + $keys = ZipUtils::updateKeys(MathHelper::castToByte($result), $keys); + $string .= chr($result); + } + unset($strBuffer); + } + + switch ($entry->getCompressionMethod()) { + case ZipEntry::COMPRESS_METHOD_DEFLATED: + $string = @gzinflate($string); + break; + case ZipEntry::COMPRESS_METHOD_STORED: + break; + default: + throw new ZipException("Compression method " . $entry->compressionMethodToString() . " not support!"); + } + $expectedCrc = sprintf('%u', crc32($string)); + if ($expectedCrc != $entry->getCrc32()) { + if ($entry->isEncrypted()) { + throw new ZipException("Wrong password"); + } + throw new ZipException("File " . $entry->getName() . ' corrupt. Bad CRC ' . dechex($expectedCrc) . ' (should be ' . dechex($entry->getCrc32()) . ')'); + } + return $string; + } + + /** + * @return string + */ + public function getArchiveComment() + { + return $this->zipComment; + } + + /** + * @param $index + * @return string + * @throws ZipException + */ + public function getCommentIndex($index) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + return $this->zipEntries[$index]->getComment(); + } + + /** + * @param string $name + * @return string + * @throws ZipException + */ + public function getCommentName($name) + { + if (!isset($this->zipEntriesIndex[$name])) { + throw new ZipException("File for name " . $name . " not found"); + } + $index = $this->zipEntriesIndex[$name]; + return $this->getCommentIndex($index); + } + + /** + * @param int $index + * @return string + * @throws ZipException + */ + public function getFromIndex($index) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + return $this->getEntryBytes($this->zipEntries[$index]); + } + + /** + * @param string $name + * @return string + * @throws ZipException + */ + public function getFromName($name) + { + if (!isset($this->zipEntriesIndex[$name])) { + throw new ZipException("File for name " . $name . " not found"); + } + $index = $this->zipEntriesIndex[$name]; + return $this->getEntryBytes($this->zipEntries[$index]); + } + + /** + * @param int $index + * @return string + * @throws ZipException + */ + public function getNameIndex($index) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + return $this->zipEntries[$index]->getName(); + } + + /** + * @param string $name + * @return bool|string + */ + public function locateName($name) + { + return isset($this->zipEntriesIndex[$name]) ? $this->zipEntriesIndex[$name] : false; + } + + /** + * @param int $index + * @param string $newName + * @return bool + * @throws ZipException + */ + public function renameIndex($index, $newName) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + $lengthNewName = strlen($newName); + if (strlen($lengthNewName) > 0xFF) { + throw new ZipException("Length new name is very long. Maximum size 255"); + } + $entry = &$this->zipEntries[$index]; + if ($entry->getName() === $newName) { + return true; + } + if (isset($this->zipEntriesIndex[$newName])) { + return false; + } + + $lengthOldName = strlen($entry->getName()); + + $this->buffer->setPosition($entry->getOffsetOfLocal() + 26); + $this->buffer->putShort($lengthNewName); + $this->buffer->skip(2); + if ($lengthOldName === $lengthNewName) { + $this->buffer->putString($newName); + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 46); + $this->buffer->putString($newName); + } else { + $this->buffer->replaceString($newName, $lengthOldName); + $diff = $lengthOldName - $lengthNewName; + + $this->offsetCentralDirectory -= $diff; + + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 28); + $this->buffer->putShort($lengthNewName); + $this->buffer->skip(16); + $this->buffer->replaceString($newName, $lengthOldName); + $this->sizeCentralDirectory -= $diff; + + $size = $this->getCountFiles(); + for ($i = $index + 1; $i < $size; $i++) { + $zipEntry = &$this->zipEntries[$i]; + $zipEntry->setOffsetOfLocal($zipEntry->getOffsetOfLocal() - $diff); + $zipEntry->setOffsetOfCentral($zipEntry->getOffsetOfCentral() - $diff); + $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 42); +// $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); +// $sig = $this->buffer->getUnsignedInt(); +// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { +// $this->buffer->skip(-4); +// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); +// } +// $this->buffer->skip(38); + $this->buffer->putInt($zipEntry->getOffsetOfLocal()); + } + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); +// $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(8); + $this->buffer->putInt($this->sizeCentralDirectory); + $this->buffer->putInt($this->offsetCentralDirectory); + } + $entry->setName($newName); + return true; + } + + /** + * @param string $name + * @param string $newName + * @return bool + * @throws ZipException + */ + public function renameName($name, $newName) + { + if (!isset($this->zipEntriesIndex[$name])) { + throw new ZipException("File for name " . $name . " not found"); + } + $index = $this->zipEntriesIndex[$name]; + return $this->renameIndex($index, $newName); + } + + /** + * @param string $comment + * @return bool + * @throws ZipException + */ + public function setArchiveComment($comment) + { + if ($comment === null) { + return false; + } + if ($comment === $this->zipComment) { + return true; + } + $currentCommentLength = strlen($this->zipComment); + $commentLength = strlen($comment); + if ($commentLength > 0xffff) { + $commentLength = 0xffff; + $comment = substr($comment, 0, $commentLength); + } + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 20); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(16); + $this->buffer->putShort($commentLength); + $this->buffer->replaceString($comment, $currentCommentLength); + + $this->zipComment = $comment; + return true; + } + + /** + * Set the comment of an entry defined by its index + * + * @param int $index + * @param string $comment + * @return bool + * @throws ZipException + */ + public function setCommentIndex($index, $comment) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + if ($comment === null) { + return false; + } + $newCommentLength = strlen($comment); + if ($newCommentLength > 0xffff) { + $newCommentLength = 0xffff; + $comment = substr($comment, 0, $newCommentLength); + } + $entry = &$this->zipEntries[$index]; + $oldComment = $entry->getComment(); + $oldCommentLength = strlen($oldComment); + $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 32); + + $this->buffer->putShort($newCommentLength); + $this->buffer->skip(12 + strlen($entry->getName()) + strlen($entry->getExtraCentral())); + + if ($oldCommentLength === $newCommentLength) { + $this->buffer->putString($comment); + } else { + $this->buffer->replaceString($comment, $oldCommentLength); + $diff = $oldCommentLength - $newCommentLength; + + $this->sizeCentralDirectory -= $diff; + $size = $this->getCountFiles(); + /** + * @var ZipEntry $entry + */ + for ($i = $index + 1; $i < $size; $i++) { + $zipEntry = &$this->zipEntries[$i]; + $zipEntry->setOffsetOfCentral($zipEntry->getOffsetOfCentral() - $diff); + } + + $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); +// $signature = $this->buffer->getUnsignedInt(); +// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { +// throw new ZipException("error position end central dir"); +// } +// $this->buffer->skip(8); + $this->buffer->putInt($this->sizeCentralDirectory); + } + $entry->setComment($comment); + return true; + } + + /** + * @return ZipEntry[] + */ + public function getZipEntries() + { + return $this->zipEntries; + } + + /** + * @param int $index + * @return ZipEntry|bool + */ + public function getZipEntryIndex($index) + { + return isset($this->zipEntries[$index]) ? $this->zipEntries[$index] : false; + } + + /** + * @param string $name + * @return ZipEntry|bool + */ + public function getZipEntryName($name) + { + return isset($this->zipEntriesIndex[$name]) ? $this->zipEntries[$this->zipEntriesIndex[$name]] : false; + } + + /** + * Set the comment of an entry defined by its name + * + * @param string $name + * @param string $comment + * @return bool + * @throws ZipException + */ + public function setCommentName($name, $comment) + { + if (!isset($this->zipEntriesIndex[$name])) { + throw new ZipException("File for name " . $name . " not found"); + } + $index = $this->zipEntriesIndex[$name]; + return $this->setCommentIndex($index, $comment); + } + + /** + * @param $index + * @return array + * @throws ZipException + */ + public function statIndex($index) + { + if (!isset($this->zipEntries[$index])) { + throw new ZipException("File for index " . $index . " not found"); + } + $entry = $this->zipEntries[$index]; + return array( + 'name' => $entry->getName(), + 'index' => $index, + 'crc' => $entry->getCrc32(), + 'size' => $entry->getUnCompressedSize(), + 'mtime' => $entry->getLastModDateTime(), + 'comp_size' => $entry->getCompressedSize(), + 'comp_method' => $entry->getCompressionMethod() + ); + } + + /** + * @param string $name + * @return array + * @throws ZipException + */ + public function statName($name) + { + if (!isset($this->zipEntriesIndex[$name])) { + throw new ZipException("File for name " . $name . " not found"); + } + $index = $this->zipEntriesIndex[$name]; + return $this->statIndex($index); + } + + public function getListFiles() + { + return array_flip($this->zipEntriesIndex); + } + + /** + * @return array + */ + public function getExtendedListFiles() + { + + return array_map(function ($index, $entry) { + /** + * @var ZipEntry $entry + * @var int $index + */ + return array( + 'name' => $entry->getName(), + 'index' => $index, + 'crc' => $entry->getCrc32(), + 'size' => $entry->getUnCompressedSize(), + 'mtime' => $entry->getLastModDateTime(), + 'comp_size' => $entry->getUnCompressedSize(), + 'comp_method' => $entry->getCompressionMethod() + ); + }, array_keys($this->zipEntries), $this->zipEntries); + } + + public function output() + { + return $this->buffer->toString(); + } + + /** + * @param string $file + * @return bool + */ + public function saveAs($file) + { + return file_put_contents($file, $this->output()) !== false; + } + + /** + * @return bool + */ + public function save() + { + if ($this->filename !== NULL) { + return file_put_contents($this->filename, $this->output()) !== false; + } + return false; + } + + public function close() + { + if ($this->buffer !== null) { + ($this->buffer instanceof ResourceBuffer) && $this->buffer->close(); + } + $this->zipEntries = null; + $this->zipEntriesIndex = null; + $this->zipComment = null; + $this->buffer = null; + $this->filename = null; + $this->offsetCentralDirectory = null; + } + + function __destruct() + { + $this->close(); + } + + private static function convertGlobToRegEx($pattern) + { + $pattern = trim($pattern, '*'); // Remove beginning and ending * globs because they're useless + $escaping = false; + $inCurlies = 0; + $chars = str_split($pattern); + $sb = ''; + foreach ($chars AS $currentChar) { + switch ($currentChar) { + case '*': + $sb .= ($escaping ? "\\*" : '.*'); + $escaping = false; + break; + case '?': + $sb .= ($escaping ? "\\?" : '.'); + $escaping = false; + break; + case '.': + case '(': + case ')': + case '+': + case '|': + case '^': + case '$': + case '@': + case '%': + $sb .= '\\' . $currentChar; + $escaping = false; + break; + case '\\': + if ($escaping) { + $sb .= "\\\\"; + $escaping = false; + } else { + $escaping = true; + } + break; + case '{': + if ($escaping) { + $sb .= "\\{"; + } else { + $sb = '('; + $inCurlies++; + } + $escaping = false; + break; + case '}': + if ($inCurlies > 0 && !$escaping) { + $sb .= ')'; + $inCurlies--; + } else if ($escaping) + $sb = "\\}"; + else + $sb = "}"; + $escaping = false; + break; + case ',': + if ($inCurlies > 0 && !$escaping) { + $sb .= '|'; + } else if ($escaping) + $sb .= "\\,"; + else + $sb = ","; + break; + default: + $escaping = false; + $sb .= $currentChar; + } + } + return $sb; + } + +} diff --git a/src/ZipUtils.php b/src/ZipUtils.php new file mode 100644 index 0000000..c54b6b9 --- /dev/null +++ b/src/ZipUtils.php @@ -0,0 +1,104 @@ +>> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff]) + * + * @param int $oldCrc + * @param int $charAt + * @return int|string + */ + public static function crc32($oldCrc, $charAt) + { + return MathHelper::castToInt(MathHelper::bitwiseXor(MathHelper::unsignedRightShift32($oldCrc, 8), self::$CRC_TABLE[MathHelper::bitwiseAnd(MathHelper::bitwiseXor($oldCrc, $charAt), 0xff)])); + } + + public static function decryptByte($byte) + { + $temp = $byte | 2; + return MathHelper::unsignedRightShift32($temp * ($temp ^ 1), 8); + } + + public static function humanSize($size, $unit = "") + { + if ((!$unit && $size >= 1 << 30) || $unit == "GB") + return number_format($size / (1 << 30), 2) . "GB"; + if ((!$unit && $size >= 1 << 20) || $unit == "MB") + return number_format($size / (1 << 20), 2) . "MB"; + if ((!$unit && $size >= 1 << 10) || $unit == "KB") + return number_format($size / (1 << 10), 2) . "KB"; + return number_format($size) . " bytes"; + } + + +} \ No newline at end of file diff --git a/tests/TestZipFile.php b/tests/TestZipFile.php new file mode 100644 index 0000000..f6ae4a4 --- /dev/null +++ b/tests/TestZipFile.php @@ -0,0 +1,244 @@ +create(); + $zip->addFile(__FILE__); + $zip->addFromString($listFilename, $listFileContent); + $zip->addEmptyDir($dirName); + $zip->setArchiveComment($archiveComment); + $zip->setCommentIndex(0, $commentIndex0); + $zip->saveAs($output); + $zip->close(); + + $this->assertTrue(file_exists($output)); + $this->assertCorrectZipArchive($output); + + $zip = new \Nelexa\Zip\ZipFile(); + $zip->open($output); + $listFiles = $zip->getListFiles(); + + $this->assertEquals(sizeof($listFiles), 3); + $filenameIndex0 = basename(__FILE__); + $this->assertEquals($listFiles[0], $filenameIndex0); + $this->assertEquals($listFiles[1], $listFilename); + $this->assertEquals($listFiles[2], $dirName); + + $this->assertEquals($zip->getFromIndex(0), $zip->getFromName(basename(__FILE__))); + $this->assertEquals($zip->getFromIndex(0), file_get_contents(__FILE__)); + $this->assertEquals($zip->getFromIndex(1), $zip->getFromName($listFilename)); + $this->assertEquals($zip->getFromIndex(1), $listFileContent); + + $this->assertEquals($zip->getArchiveComment(), $archiveComment); + $this->assertEquals($zip->getCommentIndex(0), $commentIndex0); + + if (!file_exists($extractOutputDir)) { + $this->assertTrue(mkdir($extractOutputDir, 0755, true)); + } + + $zip->extractTo($extractOutputDir); + + $this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0)); + $this->assertEquals(md5_file($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0), md5_file(__FILE__)); + + $this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename)); + $this->assertEquals(file_get_contents($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename), $listFileContent); + + $zip->close(); + + unlink($output); + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($extractOutputDir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileInfo) { + $todo = ($fileInfo->isDir() ? 'rmdir' : 'unlink'); + $todo($fileInfo->getRealPath()); + } + + rmdir($extractOutputDir); + } + + /** + * + */ + public function testUpdate() + { + $file = __DIR__ . '/res/file.apk'; + $privateKey = __DIR__ . '/res/private.pem'; + $publicKey = __DIR__ . '/res/public.pem'; + $outputFile = sys_get_temp_dir() . '/test-update.apk'; + + $zip = new \Nelexa\Zip\ZipFile($file); + $zip->open($file); + + // signed apk file + $certList = array(); + $manifestMf = new Manifest(); + $manifestMf->appendLine("Manifest-Version: 1.0"); + $manifestMf->appendLine("Created-By: 1.0 (Android)"); + $manifestMf->appendLine(''); + for ($i = 0, $length = $zip->getCountFiles(); $i < $length; $i++) { + $name = $zip->getNameIndex($i); + if ($name[strlen($name) - 1] === '/') continue; // is path + $content = $zip->getFromIndex($i); + + $certManifest = $this->createSha1EncodeEntryManifest($name, $content); + $manifestMf->appendManifest($certManifest); + $certList[$name] = $certManifest; + } + $manifestMf = $manifestMf->getContent(); + + $certSf = new Manifest(); + $certSf->appendLine('Signature-Version: 1.0'); + $certSf->appendLine('Created-By: 1.0 (Android)'); + $certSf->appendLine('SHA1-Digest-Manifest: ' . base64_encode(sha1($manifestMf, 1))); + $certSf->appendLine(''); + foreach ($certList AS $filename => $content) { + $certManifest = $this->createSha1EncodeEntryManifest($filename, $content->getContent()); + $certSf->appendManifest($certManifest); + } + $certSf = $certSf->getContent(); + unset($certList); + + $zip->addFromString('META-INF/MANIFEST.MF', $manifestMf); + $zip->addFromString('META-INF/CERT.SF', $certSf); + + if (`which openssl`) { + $openssl_cmd = 'printf ' . escapeshellarg($certSf) . ' | openssl smime -md sha1 -sign -inkey ' . escapeshellarg($privateKey) . ' -signer ' . $publicKey . ' -binary -outform DER -noattr'; + + ob_start(); + passthru($openssl_cmd, $error); + $rsaContent = ob_get_clean(); + $this->assertEquals($error, 0); + + $zip->addFromString('META-INF/CERT.RSA', $rsaContent); + } + + $zip->saveAs($outputFile); + $zip->close(); + + $this->assertCorrectZipArchive($outputFile); + + if (`which jarsigner`) { + ob_start(); + passthru('jarsigner -verify -verbose -certs ' . escapeshellarg($outputFile), $error); + $verifedResult = ob_get_clean(); + + $this->assertEquals($error, 0); + $this->assertContains('jar verified', $verifedResult); + } + + unlink($outputFile); + } + + /** + * @param $filename + */ + private function assertCorrectZipArchive($filename) + { + exec("zip -T " . escapeshellarg($filename), $output, $returnCode); + $this->assertEquals($returnCode, 0); + } + + /** + * @param string $filename + * @param string $content + * @return Manifest + */ + private function createSha1EncodeEntryManifest($filename, $content) + { + $manifest = new Manifest(); + $manifest->appendLine('Name: ' . $filename); + $manifest->appendLine('SHA1-Digest: ' . base64_encode(sha1($content, 1))); + return $manifest; + } +} + +class Manifest +{ + private $content; + + /** + * @return mixed + */ + public function getContent() + { + return trim($this->content) . "\r\n\r\n"; + } + + /** + * Process a long manifest line and add continuation if required + * @param $line string + * @return Manifest + */ + public function appendLine($line) + { + $begin = 0; + $sb = ''; + $lineLength = mb_strlen($line, "UTF-8"); + for ($end = 70; $lineLength - $begin > 70; $end += 69) { + $sb .= mb_substr($line, $begin, $end - $begin, "UTF-8") . "\r\n "; + $begin = $end; + } + $this->content .= $sb . mb_substr($line, $begin, $lineLength, "UTF-8") . "\r\n"; + return $this; + } + + public function appendManifest(Manifest $manifest) + { + $this->content .= $manifest->getContent(); + return $this; + } + + public function clear() + { + $this->content = ''; + } + + /** + * @param string $manifestContent + * @return Manifest + */ + public static function createFromManifest($manifestContent) + { + $manifestContent = trim($manifestContent); + $lines = explode("\n", $manifestContent); + + // normalize manifest + $content = ''; + $trim = array("\r", "\n"); + foreach ($lines AS $line) { + + $line = str_replace($trim, '', $line); + if ($line[0] === ' ') { + $content = rtrim($content, "\n\r"); + $line = ltrim($line); + } + $content .= $line . "\r\n"; + } + + $manifset = new self; + $lines = explode("\n", $content); + foreach ($lines AS $line) { + $line = trim($line, "\n\r"); + $manifset->appendLine($line); + } + return $manifset; + } +} \ No newline at end of file diff --git a/tests/res/file.apk b/tests/res/file.apk new file mode 100644 index 0000000000000000000000000000000000000000..d58c64b731a65789d33a174b7a64f91e968591d2 GIT binary patch literal 31742 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 LZ81Fm_yGP73^EL$ literal 0 HcmV?d00001 diff --git a/tests/res/private.pem b/tests/res/private.pem new file mode 100644 index 0000000..0af9845 --- /dev/null +++ b/tests/res/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwiURw5w8TAS0G +iWaBzJqbQyacFsIzKc+orHDzBdBdKlrx/aUlw3fBA310aP7343hc7vMm6koar/1n +8iEh2W4retDHzNf9j3IXETa5/D+3WJY4aVJkYcFr8v6OEnuSXIwKZduMeL4BpQwM +Xu/z6gkoTa9o+Dzhl46l71UX8umdPIx/4YWwX2oSm6EGklcGJyYdqMvwOXVXDE/J +qqPzC7ZQiu422cDDvqBYt32CVyjLKCo5YjWBb24jxjtl5M4y8xPOcHlVEG2WwPBU +sv2aw4Z9/Q0erZ35gMyzu+Y+2623B3tuAbfsswgBINq0bl2v+M17Kpv2tjhMKj1H +ds6CK/BVAgMBAAECggEAdTUt86f1IjENq+Fd5Z/qplsXL1sM5NtFvD+BXljl1nVg +nHpDQ6dbwxKGINv1LLAiIdGkLpovSTi/jlv8E3VA6C1KoN0oKnkqzpXnN+R6iUiP +tDR5N5yPxxQ2Xi13Td2UPPMTqVghDwZ90VjXB6LDIbcyVwc5pK3zT8hvPs9Qu8t1 +S2pCEKcowvTRSB1DMTZ3lrNjEEIMdV0H8Qik3lf7ognRGoDywu5pA1bc/Yg+XlmP +/ZmQinFeg3izNQzDdP6Ppo1i/QFeVXVuMs2ergMMHJRNUhBXKz8iNyVupqfroE8a +xRpD3eO+KvSNb0TJR5TXf64t62zEEpHaRsmgACEMAQKBgQDXo0jVUa67oqfJBFBU +3zfHIlgcrydRE4Av+LJ0hdEnFpMwVpUihJXUaJijawTzTKOgpVImhxfr/T1HMalm +MTXH5Tc7inJTiB9A1IffLPqgoOr2JRwQ2q8lgWkQPkq1ySd+q0vhkj1tuAe3qI1i +jiMo1Vb9zdVjcxmvPnZRKJgiIQKBgQDRlFm6PKc2Zx46BXeNPtXnHhSduUBJf2iO +n9/pKTANQuDlPwC3Q4edSKe44fZ/oj4KRAnzX254wXBMX+ktKX/kqXbwEanxcd/v +Lnvgv8QhsEKO3Ye09yasAfC2lYsSVSwHv+dYurb0nZ2JEPL1IP+V76RgTbdeMdic +Mt53jN/vtQKBgQC+D+mOO+Sq9X61qtuzMtvS5O6MucUJrQp7PdTs51WmAjvRiz7/ +oaT+BwMiZp2CZLaETbLOypvHIPn12kvZCt7ARcQc8rY58ey6E5l+mAJ/udXfBm5q +XJWrlRipfH4VJCtvdkP3mhIStvX2ZtXXXDiZMRDvu5CtizHESGW4uvL8gQKBgCI7 +ukBagfG3/E7776BJwETlO/bbeK3Iuvp5EOkUCj5QS04G8YX96Nv/Ly5a8pm8lae1 +n256ix/8cOx4yizPV42xRLVIHVtL/4khLajzigT6tpSBiRY9PLriAkDAwpu2/98w +MIjkzte8Gyx1cUorHrSOFWqJp0cim0BAauhaQYX1AoGAPvb5TG/A6LhYKgCWUMOH +lucrnV3Ns1BzaaMmOaokuYGtyTv2loVlrv+7QGdC9UBRXDz46DTE7CPHBwReNnWB +R7YW50VwB9YfD7dqRM24Y08F3B7RCNhqsAnpAtVgXf+/01o2nfJbzxTty/STiBNB +OjjxKHnAgIfhe7xAIiY2eow= +-----END PRIVATE KEY----- diff --git a/tests/res/public.pem b/tests/res/public.pem new file mode 100644 index 0000000..e07374f --- /dev/null +++ b/tests/res/public.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIEDeVWNjANBgkqhkiG9w0BAQUFADBtMQswCQYDVQQGEwJT +QTEPMA0GA1UECBMGUml5YWRoMQ8wDQYDVQQHEwZSaXlhZGgxEDAOBgNVBAoTB1lv +dXR5cGUxEDAOBgNVBAsTB1lvdXR5cGUxGDAWBgNVBAMTD0x1Y2llbm5lIEFuc2Vs +YTAeFw0xNjA5MDgxNDM2MjJaFw00NDAxMjUxNDM2MjJaMG0xCzAJBgNVBAYTAlNB +MQ8wDQYDVQQIEwZSaXlhZGgxDzANBgNVBAcTBlJpeWFkaDEQMA4GA1UEChMHWW91 +dHlwZTEQMA4GA1UECxMHWW91dHlwZTEYMBYGA1UEAxMPTHVjaWVubmUgQW5zZWxh +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsIlEcOcPEwEtBolmgcya +m0MmnBbCMynPqKxw8wXQXSpa8f2lJcN3wQN9dGj+9+N4XO7zJupKGq/9Z/IhIdlu +K3rQx8zX/Y9yFxE2ufw/t1iWOGlSZGHBa/L+jhJ7klyMCmXbjHi+AaUMDF7v8+oJ +KE2vaPg84ZeOpe9VF/LpnTyMf+GFsF9qEpuhBpJXBicmHajL8Dl1VwxPyaqj8wu2 +UIruNtnAw76gWLd9glcoyygqOWI1gW9uI8Y7ZeTOMvMTznB5VRBtlsDwVLL9msOG +ff0NHq2d+YDMs7vmPtuttwd7bgG37LMIASDatG5dr/jNeyqb9rY4TCo9R3bOgivw +VQIDAQABoyEwHzAdBgNVHQ4EFgQUEPoIQyYzpjseEK7hqm6UALvjJj8wDQYJKoZI +hvcNAQEFBQADggEBAD/C/48B4MvF2WzhMtLIAWuhtp73xBy6GCQBKT1dn9dgtXfD +LuHAvkx28CoOTso4Ia+JhWuu7jGfYdtL00ezV8d8Ma1k/SJfWyHpgDDk1MEhvn+h +tOoUQpt0S+QhKFxDm+INv2zw/P/TDIIodHQqkX+YVSLQMhUGRTq3vhDnfJqedAUr +QIhZjCZx9VjjiM4yhcabKEHpxqLQOcoeHB8zchnP1j/N+QSIW6hICqjcPLPLzpPu +M0RmEuRYz3EJ2P3jINhaCLFRLHTnoN2lVDS32v5Cr+IC7A1hPUcHG+07junRMEiG +uTYj9+UYI6phGJBABfFp7/oxs080RXCrKUhR+Go= +-----END CERTIFICATE-----