diff --git a/README.md b/README.md index 68bd70e..1f471c3 100644 --- a/README.md +++ b/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(); diff --git a/composer.json b/composer.json index 5022a12..9ff5fb9 100644 --- a/composer.json +++ b/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" diff --git a/src/PhpZip/ZipOutputFile.php b/src/PhpZip/ZipOutputFile.php index 57bfdfe..6b8da50 100644 --- a/src/PhpZip/ZipOutputFile.php +++ b/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); } } diff --git a/tests/PhpZip/ZipTest.php b/tests/PhpZip/ZipTest.php index 384a754..3003744 100644 --- a/tests/PhpZip/ZipTest.php +++ b/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). */ diff --git a/tests/PhpZip/ZipTestCase.php b/tests/PhpZip/ZipTestCase.php index a762512..2497db2 100644 --- a/tests/PhpZip/ZipTestCase.php +++ b/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; + } + } + } \ No newline at end of file