Explorar o código

Add support set zip alignment (alternative `zipalign` tool)

Ne-Lexa %!s(int64=9) %!d(string=hai) anos
pai
achega
f0d90da75c
Modificáronse 5 ficheiros con 110 adicións e 12 borrados
  1. 6 1
      README.md
  2. 3 2
      composer.json
  3. 47 9
      src/PhpZip/ZipOutputFile.php
  4. 38 0
      tests/PhpZip/ZipTest.php
  5. 16 0
      tests/PhpZip/ZipTestCase.php

+ 6 - 1
README.md

@@ -2,7 +2,7 @@
 ================
 `PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP.
 
-The library supports `ZIP64`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
+The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
 
 The library does not require extension `php-xml` and class `ZipArchive`.
 
@@ -415,6 +415,11 @@ while ($iterator->valid())
     $iterator->next();
 }
 ```
+Set zip alignment (alternate program `zipalign`).
+```php
+// before save or output
+$zipOutputFile->setAlign(4); // alternative cmd: zipalign -f -v 4 filename.zip
+```
 Close zip archive.
 ```php
 $zipOutputFile->close();

+ 3 - 2
composer.json

@@ -1,12 +1,13 @@
 {
   "name": "nelexa/zip",
-  "description": "Zip files CRUD. Open, create, update, extract and get info tool. Support read and write encrypted archives. Support ZIP64 ext. Alternative ZipArchive. It does not require php-zip extension.",
+  "description": "Zip files CRUD. Open, create, update, extract and get info tool. Support read and write encrypted archives. Support ZIP64 ext and zip align. Alternative ZipArchive. It does not require php-zip extension.",
   "type": "library",
   "keywords": [
     "zip",
     "archive",
     "extract",
-    "winzip"
+    "winzip",
+    "zipalign"
   ],
   "require-dev": {
     "phpunit/phpunit": "4.8"

+ 47 - 9
src/PhpZip/ZipOutputFile.php

@@ -103,6 +103,13 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
      */
     private $level = self::LEVEL_DEFAULT_COMPRESSION;
 
+    /**
+     * ZipAlign setting
+     *
+     * @var int
+     */
+    private $align;
+
     /**
      * ZipOutputFile constructor.
      * @param ZipFile|null $zipFile
@@ -805,6 +812,18 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
         $this->level = $level;
     }
 
+    /**
+     * @param int|null $align
+     */
+    public function setZipAlign($align = 4)
+    {
+        if ($align === null) {
+            $this->align = null;
+            return;
+        }
+        $this->align = (int)$align;
+    }
+
     /**
      * Save as file
      *
@@ -999,6 +1018,10 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
 
         $offset = ftell($outputHandle);
 
+        // Commit changes.
+        $entry->setGeneralPurposeBitFlags($general);
+        $entry->setRawOffset($offset);
+
         // Start changes.
         // local file header signature     4 bytes  (0x04034b50)
         // version needed to extract       2 bytes
@@ -1012,6 +1035,22 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
         // file name length                2 bytes
         // extra field length              2 bytes
         $extra = $entry->getRawExtraFields();
+
+        // zip align
+        $padding = 0;
+        if ($this->align !== null && !$entry->isEncrypted() && $entry->getMethod() === ZipEntry::METHOD_STORED) {
+            $padding =
+                (
+                    $this->align -
+                    (
+                        $offset +
+                        self::LOCAL_FILE_HEADER_MIN_LEN +
+                        strlen($entry->getName()) +
+                        strlen($extra)
+                    ) % $this->align
+                ) % $this->align;
+        }
+
         fwrite($outputHandle, pack('VvvvVVVVvv',
             ZipConstants::LOCAL_FILE_HEADER_SIG,
             $entry->getVersionNeededToExtract(),
@@ -1022,15 +1061,16 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
             $dd ? 0 : (int)$entry->getRawCompressedSize(),
             $dd ? 0 : (int)$entry->getRawSize(),
             strlen($entry->getName()),
-            strlen($extra)
+            strlen($extra) + $padding
         ));
         // file name (variable size)
         fwrite($outputHandle, $entry->getName());
         // extra field (variable size)
         fwrite($outputHandle, $extra);
-        // Commit changes.
-        $entry->setGeneralPurposeBitFlags($general);
-        $entry->setRawOffset($offset);
+
+        if ($padding > 0) {
+            fwrite($outputHandle, str_repeat(chr(0), $padding));
+        }
 
         fwrite($outputHandle, $entryContent);
 
@@ -1059,7 +1099,6 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
                 . " (expected compressed entry size of "
                 . $entry->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
         }
-
     }
 
     /**
@@ -1321,13 +1360,12 @@ class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
      */
     public function offsetSet($entryName, $uncompressedDataContent)
     {
-        if(empty($entryName)){
+        if (empty($entryName)) {
             throw new IllegalArgumentException('Entry name empty');
         }
-        if($entryName[strlen($entryName)-1] === '/'){
+        if ($entryName[strlen($entryName) - 1] === '/') {
             $this->addEmptyDir($entryName);
-        }
-        else{
+        } else {
             $this->addFromString($entryName, $uncompressedDataContent);
         }
     }

+ 38 - 0
tests/PhpZip/ZipTest.php

@@ -1022,6 +1022,44 @@ class ZipTest extends ZipTestCase
         $zipFile->close();
     }
 
+    public function testZipAlign()
+    {
+        $zipOutputFile = ZipOutputFile::create();
+
+        for ($i = 0; $i < 100; $i++) {
+            $zipOutputFile->addFromString(
+                'entry' . $i . '.txt',
+                CryptoUtil::randomBytes(mt_rand(100, 4096)),
+                ZipEntry::METHOD_STORED
+            );
+        }
+        $zipOutputFile->saveAsFile($this->outputFilename);
+        $zipOutputFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $result = self::doZipAlignVerify($this->outputFilename);
+        if($result === null) return; // zip align not installed
+
+        // check not zip align
+        self::assertFalse($result, false);
+
+        $zipFile = ZipFile::openFromFile($this->outputFilename);
+        $zipOutputFile = ZipOutputFile::openFromZipFile($zipFile);
+        $zipOutputFile->setZipAlign(4);
+        $zipOutputFile->saveAsFile($this->outputFilename);
+        $zipOutputFile->close();
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $result = self::doZipAlignVerify($this->outputFilename);
+        self::assertNotNull($result);
+
+        // check zip align
+        self::assertTrue($result);
+    }
+
     /**
      * Test support ZIP64 ext (slow test - normal).
      */

+ 16 - 0
tests/PhpZip/ZipTestCase.php

@@ -42,4 +42,20 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
         self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
     }
 
+    /**
+     * @param string $filename
+     * @return bool|null If null - can not install zipalign
+     */
+    public static function doZipAlignVerify($filename)
+    {
+        if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
+            exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
+            return $returnCode === 0;
+        } else {
+            echo 'Can not find program "zipalign" for test' . PHP_EOL;
+            fwrite(STDERR, 'Can not find program "zipalign" for test');
+            return null;
+        }
+    }
+
 }