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 0000000..d58c64b Binary files /dev/null and b/tests/res/file.apk differ 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-----