소스 검색

Zip file create and modification

Ne-Lexa 9 년 전
부모
커밋
45937d2620
12개의 변경된 파일3006개의 추가작업 그리고 0개의 파일을 삭제
  1. 3 0
      .gitignore
  2. 250 0
      README.md
  3. 25 0
      composer.json
  4. 45 0
      src/FilterFileIterator.php
  5. 905 0
      src/ZipEntry.php
  6. 7 0
      src/ZipException.php
  7. 1374 0
      src/ZipFile.php
  8. 104 0
      src/ZipUtils.php
  9. 244 0
      tests/TestZipFile.php
  10. BIN
      tests/res/file.apk
  11. 28 0
      tests/res/private.pem
  12. 21 0
      tests/res/public.pem

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/vendor/
+*.iml
+/.idea/

+ 250 - 0
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%
+```

+ 25 - 0
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"
+    }
+  }
+}

+ 45 - 0
src/FilterFileIterator.php

@@ -0,0 +1,45 @@
+<?php
+namespace Nelexa\Zip;
+
+class FilterFileIterator extends \FilterIterator
+{
+    private $ignoreFiles;
+    private static $ignoreAlways = array('..');
+
+    /**
+     * @param \Iterator $iterator
+     * @param array $ignoreFiles
+     */
+    public function __construct(\Iterator $iterator, array $ignoreFiles)
+    {
+        parent::__construct($iterator);
+        $this->ignoreFiles = array_merge(self::$ignoreAlways, $ignoreFiles);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.1.0)<br/>
+     * 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);
+    }
+}

+ 905 - 0
src/ZipEntry.php

@@ -0,0 +1,905 @@
+<?php
+namespace Nelexa\Zip;
+
+use Nelexa\Buffer\Buffer;
+use Nelexa\Buffer\StringBuffer;
+
+class ZipEntry
+{
+    // made by constants
+    const MADE_BY_MS_DOS = 0;
+    const MADE_BY_AMIGA = 1;
+    const MADE_BY_OPEN_VMS = 2;
+    const MADE_BY_UNIX = 3;
+    const MADE_BY_VM_CMS = 4;
+    const MADE_BY_ATARI = 5;
+    const MADE_BY_OS_2 = 6;
+    const MADE_BY_MACINTOSH = 7;
+    const MADE_BY_Z_SYSTEM = 8;
+    const MADE_BY_CP_M = 9;
+    const MADE_BY_WINDOWS_NTFS = 10;
+    const MADE_BY_MVS = 11;
+    const MADE_BY_VSE = 12;
+    const MADE_BY_ACORN_RISC = 13;
+    const MADE_BY_VFAT = 14;
+    const MADE_BY_ALTERNATE_MVS = 15;
+    const MADE_BY_BEOS = 16;
+    const MADE_BY_TANDEM = 17;
+    const MADE_BY_OS_400 = 18;
+    const MADE_BY_OS_X = 19;
+    const MADE_BY_UNKNOWN = 20;
+
+    private static $valuesMadeBy = array(
+        self::MADE_BY_MS_DOS => '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 . '"' .
+        '}';
+    }
+}

+ 7 - 0
src/ZipException.php

@@ -0,0 +1,7 @@
+<?php
+namespace Nelexa\Zip;
+
+class ZipException extends \Exception
+{
+
+}

+ 1374 - 0
src/ZipFile.php

@@ -0,0 +1,1374 @@
+<?php
+namespace Nelexa\Zip;
+
+use Nelexa\Buffer\Buffer;
+use Nelexa\Buffer\BufferException;
+use Nelexa\Buffer\MathHelper;
+use Nelexa\Buffer\MemoryResourceBuffer;
+use Nelexa\Buffer\ResourceBuffer;
+use Nelexa\Buffer\StringBuffer;
+
+class ZipFile
+{
+    const SIGNATURE_LOCAL_HEADER = 0x04034b50;
+    const SIGNATURE_CENTRAL_DIR = 0x02014b50;
+    const SIGNATURE_END_CENTRAL_DIR = 0x06054b50;
+
+    private static $initPwdKeys = array(305419896, 591751049, 878082192);
+
+
+    /**
+     * @var string
+     */
+    private $filename;
+    /**
+     * @var Buffer
+     */
+    private $buffer;
+    /**
+     * @var int
+     */
+    private $offsetCentralDirectory;
+    /**
+     * @var int
+     */
+    private $sizeCentralDirectory = 0;
+    /**
+     * @var ZipEntry[]
+     */
+    private $zipEntries;
+    /**
+     * @var string[]
+     */
+    private $zipEntriesIndex;
+    /**
+     * @var string
+     */
+    private $zipComment = "";
+    /**
+     * @var string
+     */
+    private $password = null;
+
+    public function __construct()
+    {
+
+    }
+
+    /**
+     * Create zip archive
+     */
+    public function create()
+    {
+        $this->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;
+    }
+
+}

+ 104 - 0
src/ZipUtils.php

@@ -0,0 +1,104 @@
+<?php
+namespace Nelexa\Zip;
+
+use Nelexa\Buffer\MathHelper;
+
+class ZipUtils
+{
+    const DECRYPT_HEADER_SIZE = 12;
+    public static $CFH_SIGNATURE = array(0x50, 0x4b, 0x01, 0x02);
+    public static $LFH_SIGNATURE = array(0x50, 0x4b, 0x03, 0x04);
+    public static $ECD_SIGNATURE = array(0x50, 0x4b, 0x05, 0x06);
+    public static $DD_SIGNATURE = array(0x50, 0x4b, 0x07, 0x08);
+
+    private static $CRC_TABLE = array(
+        0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
+        0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
+        0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
+        0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
+        0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
+        0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
+        0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
+        0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
+        0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
+        0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
+        0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
+        0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
+        0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
+        0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
+        0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
+        0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+        0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
+        0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
+        0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
+        0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
+        0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
+        0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
+        0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
+        0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
+        0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
+        0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
+        0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
+        0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+        0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
+        0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
+        0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
+        0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
+        0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
+        0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
+        0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
+        0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+        0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
+        0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
+        0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
+        0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
+        0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
+        0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
+        0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
+    );
+
+    /**
+     * @param int $charAt
+     * @param int[] $keys
+     * @return array
+     */
+    public static function updateKeys($charAt, array $keys)
+    {
+        $keys[0] = self::crc32($keys[0], $charAt);
+        $keys[1] = MathHelper::add($keys[1], MathHelper::bitwiseAnd($keys[0], 0xff));
+        $keys[1] = MathHelper::castToInt(MathHelper::add(MathHelper::mul($keys[1], 134775813), 1));
+        $keys[2] = self::crc32($keys[2], MathHelper::rightShift32($keys[1], 24));
+        return $keys;
+    }
+
+    /**
+     * Alg: ((oldCrc >>> 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";
+    }
+
+
+}

+ 244 - 0
tests/TestZipFile.php

@@ -0,0 +1,244 @@
+<?php
+
+class TestZipFile extends \PHPUnit\Framework\TestCase
+{
+
+    public function testCreate()
+    {
+        $output = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create.zip';
+        $extractOutputDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create';
+
+        $listFilename = 'files.txt';
+        $listFileContent = implode(PHP_EOL, glob('*'));
+        $dirName = 'src/';
+        $archiveComment = 'Archive comment - 😀';
+        $commentIndex0 = basename(__FILE__);
+
+        $zip = new \Nelexa\Zip\ZipFile();
+        $zip->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;
+    }
+}

BIN
tests/res/file.apk


+ 28 - 0
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-----

+ 21 - 0
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-----