1
0
mirror of https://github.com/Ne-Lexa/php-zip.git synced 2025-08-09 08:56:28 +02:00

Merge ZipFile and ZipOutputFile, optimization update archive.

This commit is contained in:
wapplay-home-linux
2017-03-10 08:23:57 +03:00
parent cc75f44949
commit f802861d86
31 changed files with 5018 additions and 4525 deletions

View File

@@ -1,12 +1,20 @@
`PhpZip` Version 2 `PhpZip` (ver 3.0.+)
================ ====================
`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP. `PhpZip` - php library for manipulating zip archives.
The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`. Features:
---------
ZIP64 extensions are automatically and transparently activated when reading or writing ZIP files of more than 4 GB size. - Opening and unzipping zip files.
- Create zip files.
The library does not require extension `php-zip` and class `ZipArchive`. - Update zip files.
- Pure php (not require extension `php-zip` and class `\ZipArchive`).
- Output the modified archive as a string or output to the browser without saving the result to disk.
- Support archive comment and entries comments.
- Get info of zip entries.
- Support zip password for PHP < 5.6.0 (`\ZipArchive` required this version), include update and remove password.
- Support encryption method `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption`.
- Support `ZIP64` (size > 4 GiB or files > 65535 in a .ZIP archive).
- Support archive alignment functional [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
Requirements Requirements
------------ ------------
@@ -17,19 +25,44 @@ Requirements
Installation Installation
------------ ------------
`composer require nelexa/zip` `composer require nelexa/zip:^3.0`
Samples
-------
```php
// create archive
$zipFile = new \PhpZip\ZipFile();
$zipFile->addFromString("zip/entry/filename", "Is file content")
->addFile("/path/to/file", "data/tofile")
->addDir(__DIR__, "to/path/")
->saveAsFile($outputFilename)
->close();
// open archive, extract, add files, set password and output to browser.
$zipFile->openFile($outputFilename)
->extractTo($outputDirExtract)
->deleteFromRegex('~^\.~') // delete all hidden (Unix) files
->addFromString('dir/file.txt', 'Test file')
->withNewPassword('password')
->outputAsAttachment('library.jar');
```
Other examples can be found in the `tests/` folder
Documentation Documentation
------------- -------------
#### Class `\PhpZip\ZipFile` (open, extract, info)
Open zip archive from file. Open zip archive from file.
```php ```php
$zipFile = \PhpZip\ZipFile::openFromFile($filename); $zipFile = new \PhpZip\ZipFile();
$zipFile->openFile($filename);
``` ```
Open zip archive from data string. Open zip archive from data string.
```php ```php
$data = file_get_contents($filename); $data = file_get_contents($urlOrFile);
$zipFile = \PhpZip\ZipFile::openFromString($data); $zipFile = new \PhpZip\ZipFile();
$zipFile->openFromString($filename);
``` ```
Open zip archive from stream resource. Open zip archive from stream resource.
```php ```php

View File

@@ -4,6 +4,7 @@
"type": "library", "type": "library",
"keywords": [ "keywords": [
"zip", "zip",
"unzip",
"archive", "archive",
"extract", "extract",
"winzip", "winzip",
@@ -23,16 +24,22 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"require": { "require": {
"php-64bit": "^5.4 || ^7.0", "php-64bit": "^5.4 || ^7.0",
"ext-mbstring": "*" "ext-mbstring": "*",
"nelexa/buffer": "^1.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"": "src/" "PhpZip\\": "src/PhpZip"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"": "tests/" "PhpZip\\": "tests/PhpZip"
} }
},
"suggest": {
"ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt",
"ext-mcrypt": "Needed to support encrypt zip entries or use ext-openssl",
"ext-bz2": "Needed to support BZIP2 compression"
} }
} }

View File

@@ -0,0 +1,24 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
interface CryptoEngine
{
/**
* Decryption string.
*
* @param string $encryptionContent
* @return string
* @throws ZipAuthenticationException
*/
public function decrypt($encryptionContent);
/**
* Encryption string.
*
* @param string $content
* @return string
*/
public function encrypt($content);
}

View File

@@ -2,6 +2,7 @@
namespace PhpZip\Crypto; namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Model\ZipEntry; use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil; use PhpZip\Util\CryptoUtil;
@@ -12,7 +13,7 @@ use PhpZip\Util\CryptoUtil;
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class TraditionalPkwareEncryptionEngine class TraditionalPkwareEncryptionEngine implements CryptoEngine
{ {
/** /**
* Encryption header size * Encryption header size
@@ -154,7 +155,7 @@ class TraditionalPkwareEncryptionEngine
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// compare against the file type from extended local headers // compare against the file type from extended local headers
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff; $checkByte = ($this->entry->getTime() >> 8) & 0xff;
} else { } else {
// compare against the CRC otherwise // compare against the CRC otherwise
$checkByte = ($this->entry->getCrc() >> 24) & 0xff; $checkByte = ($this->entry->getCrc() >> 24) & 0xff;
@@ -187,11 +188,13 @@ class TraditionalPkwareEncryptionEngine
* Encryption data * Encryption data
* *
* @param string $data * @param string $data
* @param int $crc
* @return string * @return string
*/ */
public function encrypt($data, $crc) public function encrypt($data)
{ {
$crc = ($this->entry->isDataDescriptorRequired() ?
($this->entry->getTime() & 0x0000ffff) << 16 :
$this->entry->getCrc());
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE); $headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted. // Initialize again since the generated bytes were encrypted.
@@ -206,11 +209,12 @@ class TraditionalPkwareEncryptionEngine
/** /**
* @param string $content * @param string $content
* @return string * @return string
* @throws ZipCryptoException
*/ */
private function encryptData($content) private function encryptData($content)
{ {
if ($content === null) { if ($content === null) {
throw new \RuntimeException(); throw new ZipCryptoException('content is null');
} }
$buff = ''; $buff = '';
foreach (unpack('C*', $content) as $val) { foreach (unpack('C*', $content) as $val) {
@@ -223,7 +227,7 @@ class TraditionalPkwareEncryptionEngine
* @param int $byte * @param int $byte
* @return int * @return int
*/ */
protected function encryptByte($byte) private function encryptByte($byte)
{ {
$tempVal = $byte ^ $this->decryptByte() & 0xff; $tempVal = $byte ^ $this->decryptByte() & 0xff;
$this->updateKeys($byte); $this->updateKeys($byte);

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace PhpZip\Crypto; namespace PhpZip\Crypto;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException; use PhpZip\Exception\ZipCryptoException;
use PhpZip\Extra\WinZipAesEntryExtraField; use PhpZip\Extra\WinZipAesEntryExtraField;
@@ -13,7 +14,7 @@ use PhpZip\Util\CryptoUtil;
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class WinZipAesEngine class WinZipAesEngine implements CryptoEngine
{ {
/** /**
* The block size of the Advanced Encryption Specification (AES) Algorithm * The block size of the Advanced Encryption Specification (AES) Algorithm
@@ -42,12 +43,12 @@ class WinZipAesEngine
/** /**
* Decrypt from stream resource. * Decrypt from stream resource.
* *
* @param resource $stream Input stream resource * @param string $content Input stream buffer
* @return string * @return string
* @throws ZipAuthenticationException * @throws ZipAuthenticationException
* @throws ZipCryptoException * @throws ZipCryptoException
*/ */
public function decrypt($stream) public function decrypt($content)
{ {
/** /**
* @var WinZipAesEntryExtraField $field * @var WinZipAesEntryExtraField $field
@@ -57,20 +58,20 @@ class WinZipAesEngine
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)"); throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
} }
$pos = ftell($stream);
// Get key strength. // Get key strength.
$keyStrengthBits = $field->getKeyStrength(); $keyStrengthBits = $field->getKeyStrength();
$keyStrengthBytes = $keyStrengthBits / 8; $keyStrengthBytes = $keyStrengthBits / 8;
$salt = fread($stream, $keyStrengthBytes / 2); $pos = $keyStrengthBytes / 2;
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8); $salt = substr($content, 0, $pos);
$passwordVerifier = substr($content, $pos, self::PWD_VERIFIER_BITS / 8);
$pos += self::PWD_VERIFIER_BITS / 8;
$sha1Size = 20; $sha1Size = 20;
// Init start, end and size of encrypted data. // Init start, end and size of encrypted data.
$endPos = $pos + $this->entry->getCompressedSize(); $start = $pos;
$start = ftell($stream); $endPos = strlen($content);
$footerSize = $sha1Size / 2; $footerSize = $sha1Size / 2;
$end = $endPos - $footerSize; $end = $endPos - $footerSize;
$size = $end - $start; $size = $end - $start;
@@ -80,9 +81,8 @@ class WinZipAesEngine
} }
// Load authentication code. // Load authentication code.
fseek($stream, $end, SEEK_SET); $authenticationCode = substr($content, $end, $footerSize);
$authenticationCode = fread($stream, $footerSize); if ($end + $footerSize !== $endPos) {
if (ftell($stream) !== $endPos) {
// This should never happen unless someone is writing to the // This should never happen unless someone is writing to the
// end of the file concurrently! // end of the file concurrently!
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!"); throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
@@ -95,27 +95,33 @@ class WinZipAesEngine
// WinZip 99-character limit // WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99); $password = substr($password, 0, 99);
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
do { do {
// Here comes the strange part about WinZip AES encryption: // Here comes the strange part about WinZip AES encryption:
// Its unorthodox use of the Password-Based Key Derivation // Its unorthodox use of the Password-Based Key Derivation
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898. // Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
// Yes, the password verifier is only a 16 bit value. // Yes, the password verifier is only a 16 bit value.
// So we must use the MAC for password verification, too. // So we must use the MAC for password verification, too.
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true); $keyParam = hash_pbkdf2(
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8; "sha1",
$iv = str_repeat(chr(0), $ctrIvSize); $password,
$salt,
self::ITERATION_COUNT,
(2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8,
true
);
$key = substr($keyParam, 0, $keyStrengthBytes); $key = substr($keyParam, 0, $keyStrengthBytes);
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes); $sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
// Verify password. // Verify password.
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes)); } while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
$content = stream_get_contents($stream, $size, $start); $content = substr($content, $start, $size);
$mac = hash_hmac('sha1', $content, $sha1MacParam, true); $mac = hash_hmac('sha1', $content, $sha1MacParam, true);
if ($authenticationCode !== substr($mac, 0, 10)) { if ($authenticationCode !== substr($mac, 0, 10)) {
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)"); throw new ZipAuthenticationException($this->entry->getName() .
" (authenticated WinZip AES entry content has been tampered with)");
} }
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv); return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
@@ -161,6 +167,7 @@ class WinZipAesEngine
* @param string $key Aes key * @param string $key Aes key
* @param string $iv Aes IV * @param string $iv Aes IV
* @return string Encrypted data * @return string Encrypted data
* @throws RuntimeException
*/ */
private static function encryptCtr($data, $key, $iv) private static function encryptCtr($data, $key, $iv)
{ {
@@ -170,7 +177,7 @@ class WinZipAesEngine
} elseif (extension_loaded("mcrypt")) { } elseif (extension_loaded("mcrypt")) {
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else { } else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
} }
} }
@@ -181,6 +188,7 @@ class WinZipAesEngine
* @param string $key Aes key * @param string $key Aes key
* @param string $iv Aes IV * @param string $iv Aes IV
* @return string Raw data * @return string Raw data
* @throws RuntimeException
*/ */
private static function decryptCtr($data, $key, $iv) private static function decryptCtr($data, $key, $iv)
{ {
@@ -190,7 +198,7 @@ class WinZipAesEngine
} elseif (extension_loaded("mcrypt")) { } elseif (extension_loaded("mcrypt")) {
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else { } else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
} }
} }

View File

@@ -8,7 +8,7 @@ namespace PhpZip\Exception;
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class IllegalArgumentException extends ZipException class InvalidArgumentException extends ZipException
{ {
} }

View File

@@ -0,0 +1,13 @@
<?php
namespace PhpZip\Exception;
/**
* Runtime exception.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class RuntimeException extends ZipException
{
}

View File

@@ -80,7 +80,7 @@ abstract class ExtraField implements ExtraFieldHeader
if (0x0000 > $size || $size > 0xffff) { if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size data block out of range.'); throw new ZipException('size data block out of range.');
} }
$fp = fopen('php://temp', 'r+b'); $fp = fopen('php://memory', 'r+b');
if (0 === $size) return $fp; if (0 === $size) return $fp;
$this->writeTo($fp, 0); $this->writeTo($fp, 0);
rewind($fp); rewind($fp);

View File

@@ -1,7 +1,6 @@
<?php <?php
namespace PhpZip\Extra; namespace PhpZip\Extra;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
/** /**
@@ -118,8 +117,17 @@ class ExtraFields
} }
if (0 === $size) return ''; if (0 === $size) return '';
$fp = fopen('php://temp', 'r+b'); $fp = fopen('php://memory', 'r+b');
$this->writeTo($fp, 0); $offset = 0;
/**
* @var ExtraField $ef
*/
foreach ($this->extra as $ef) {
fwrite($fp, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
$offset += 4;
fwrite($fp, $ef->writeTo($fp, $offset));
$offset += $ef->getDataSize();
}
rewind($fp); rewind($fp);
$content = stream_get_contents($fp); $content = stream_get_contents($fp);
fclose($fp); fclose($fp);
@@ -148,27 +156,6 @@ class ExtraFields
return $length; return $length;
} }
/**
* Serializes a list of Extra Fields of ExtraField::getExtraLength bytes to the
* stream resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset
*/
private function writeTo($handle, $off)
{
fseek($handle, $off, SEEK_SET);
/**
* @var ExtraField $ef
*/
foreach ($this->extra as $ef) {
fwrite($handle, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
$off += 4;
fwrite($handle, $ef->writeTo($handle, $off));
$off += $ef->getDataSize();
}
}
/** /**
* Initializes this Extra Field by deserializing a Data Block of * Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off. * size bytes $size from the resource $handle at the zero based offset $off.
@@ -187,7 +174,7 @@ class ExtraFields
if (null !== $handle && 0 < $size) { if (null !== $handle && 0 < $size) {
$end = $off + $size; $end = $off + $size;
while ($off < $end) { while ($off < $end) {
fseek($handle, $off, SEEK_SET); fseek($handle, $off);
$unpack = unpack('vheaderId/vdataSize', fread($handle, 4)); $unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
$off += 4; $off += 4;
$extraField = ExtraField::create($unpack['headerId']); $extraField = ExtraField::create($unpack['headerId']);

View File

@@ -0,0 +1,466 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Model\Entry\ZipNewStringEntry;
use PhpZip\Model\Entry\ZipReadEntry;
use PhpZip\ZipFile;
/**
* Read Central Directory
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class CentralDirectory
{
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
/**
* @var EndOfCentralDirectory End of Central Directory
*/
private $endOfCentralDirectory;
/**
* @var ZipEntry[] Maps entry names to zip entries.
*/
private $entries = [];
/**
* @var ZipEntry[] New and modified entries
*/
private $modifiedEntries = [];
/**
* @var int Default compression level for the methods DEFLATED and BZIP2.
*/
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
/**
* @var int|null ZipAlign setting
*/
private $zipAlign;
/**
* @var string New password
*/
private $password;
/**
* @var int
*/
private $encryptionMethod;
/**
* @var bool
*/
private $clearPassword;
public function __construct()
{
$this->endOfCentralDirectory = new EndOfCentralDirectory();
}
/**
* Reads the central directory from the given seekable byte channel
* and populates the internal tables with ZipEntry instances.
*
* The ZipEntry's will know all data that can be obtained from the
* central directory alone, but not the data that requires the local
* file header or additional data to be read.
*
* @param resource $inputStream
* @throws ZipException
*/
public function mountCentralDirectory($inputStream)
{
$this->modifiedEntries = [];
$this->checkZipFileSignature($inputStream);
$this->endOfCentralDirectory->findCentralDirectory($inputStream);
$numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
$entries = [];
for (; $numEntries > 0; $numEntries--) {
$entry = new ZipReadEntry($inputStream);
$entry->setCentralDirectory($this);
// Re-load virtual offset after ZIP64 Extended Information
// Extra Field may have been parsed, map it to the real
// offset and conditionally update the preamble size from it.
$lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset());
if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
$this->endOfCentralDirectory->setPreamble($lfhOff);
}
$entries[$entry->getName()] = $entry;
}
if (0 !== $numEntries % 0x10000) {
throw new ZipException("Expected " . abs($numEntries) .
($numEntries > 0 ? " more" : " less") .
" entries in the Central Directory!");
}
$this->entries = $entries;
if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
assert(0 === $numEntries);
$this->checkZipFileSignature($inputStream);
}
}
/**
* Check zip file signature
*
* @param resource $inputStream
* @throws ZipException if this not .ZIP file.
*/
private function checkZipFileSignature($inputStream)
{
rewind($inputStream);
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
$signature = unpack('V', fread($inputStream, 4))[1];
if (
ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
&& EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
&& EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
) {
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
}
}
/**
* Set compression method for new or rewrites entries.
* @param int $compressionLevel
* @throws InvalidArgumentException
* @see ZipFile::LEVEL_DEFAULT_COMPRESSION
* @see ZipFile::LEVEL_BEST_SPEED
* @see ZipFile::LEVEL_BEST_COMPRESSION
*/
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
{
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
}
$this->compressionLevel = $compressionLevel;
}
/**
* @return ZipEntry[]
*/
public function &getEntries()
{
return $this->entries;
}
/**
* @param string $entryName
* @return ZipEntry
* @throws ZipNotFoundEntry
*/
public function getEntry($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
return $this->entries[$entryName];
}
/**
* @return EndOfCentralDirectory
*/
public function getEndOfCentralDirectory()
{
return $this->endOfCentralDirectory;
}
public function getArchiveComment()
{
return null === $this->endOfCentralDirectory->getComment() ?
'' :
$this->endOfCentralDirectory->getComment();
}
/**
* Set entry comment
* @param string $entryName
* @param string|null $comment
* @throws ZipNotFoundEntry
*/
public function setEntryComment($entryName, $comment)
{
if (isset($this->modifiedEntries[$entryName])) {
$this->modifiedEntries[$entryName]->setComment($comment);
} elseif (isset($this->entries[$entryName])) {
$entry = clone $this->entries[$entryName];
$entry->setComment($comment);
$this->putInModified($entryName, $entry);
} else {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
}
/**
* @param string|null $password
* @param int|null $encryptionMethod
*/
public function setNewPassword($password, $encryptionMethod = null)
{
$this->password = $password;
$this->encryptionMethod = $encryptionMethod;
$this->clearPassword = $password === null;
}
/**
* @return int|null
*/
public function getZipAlign()
{
return $this->zipAlign;
}
/**
* @param int|null $zipAlign
*/
public function setZipAlign($zipAlign = null)
{
if ($zipAlign === null) {
$this->zipAlign = null;
return;
}
$this->zipAlign = (int)$zipAlign;
}
/**
* Put modification or new entries.
*
* @param $entryName
* @param ZipEntry $entry
*/
public function putInModified($entryName, ZipEntry $entry)
{
$this->modifiedEntries[$entryName] = $entry;
}
/**
* @param string $entryName
* @throws ZipNotFoundEntry
*/
public function deleteEntry($entryName)
{
if (isset($this->entries[$entryName])) {
$this->modifiedEntries[$entryName] = null;
} elseif (isset($this->modifiedEntries[$entryName])) {
unset($this->modifiedEntries[$entryName]);
} else {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
}
/**
* @param string $regexPattern
* @return bool
*/
public function deleteEntriesFromRegex($regexPattern)
{
$count = 0;
foreach ($this->modifiedEntries as $entryName => &$entry) {
if (preg_match($regexPattern, $entryName)) {
unset($entry);
$count++;
}
}
foreach ($this->entries as $entryName => $entry) {
if (preg_match($regexPattern, $entryName)) {
$this->modifiedEntries[$entryName] = null;
$count++;
}
}
return $count > 0;
}
/**
* @param string $oldName
* @param string $newName
* @throws InvalidArgumentException
* @throws ZipNotFoundEntry
*/
public function rename($oldName, $newName)
{
$oldName = (string)$oldName;
$newName = (string)$newName;
if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
}
if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
$newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
$this->modifiedEntries[$oldName] :
$this->entries[$oldName]);
$newEntry->setName($newName);
$this->modifiedEntries[$oldName] = null;
$this->modifiedEntries[$newName] = $newEntry;
return;
}
throw new ZipNotFoundEntry("Not found entry " . $oldName);
}
/**
* Delete all entries.
*/
public function deleteAll()
{
$this->modifiedEntries = [];
foreach ($this->entries as $entry) {
$this->modifiedEntries[$entry->getName()] = null;
}
}
/**
* @param resource $outputStream
*/
public function writeArchive($outputStream)
{
/**
* @var ZipEntry[] $memoryEntriesResult
*/
$memoryEntriesResult = [];
foreach ($this->entries as $entryName => $entry) {
if (isset($this->modifiedEntries[$entryName])) continue;
if (
($this->password !== null || $this->clearPassword) &&
$entry->isEncrypted() &&
$entry->getPassword() !== null &&
(
$entry->getPassword() !== $this->password ||
$entry->getEncryptionMethod() !== $this->encryptionMethod
)
) {
$prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
$prototypeEntry->setName($entry->getName());
$prototypeEntry->setMethod($entry->getMethod());
$prototypeEntry->setTime($entry->getTime());
$prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
$prototypeEntry->setExtra($entry->getExtra());
$prototypeEntry->setPassword($this->password, $this->encryptionMethod);
if($this->clearPassword){
$prototypeEntry->clearEncryption();
}
} else {
$prototypeEntry = clone $entry;
}
$memoryEntriesResult[$entryName] = $prototypeEntry;
}
foreach ($this->modifiedEntries as $entryName => $outputEntry) {
if (null === $outputEntry) { // remove marked entry
unset($memoryEntriesResult[$entryName]);
} else {
if ($this->password !== null) {
$outputEntry->setPassword($this->password, $this->encryptionMethod);
}
$memoryEntriesResult[$entryName] = $outputEntry;
}
}
foreach ($memoryEntriesResult as $key => $outputEntry) {
$outputEntry->setCentralDirectory($this);
$outputEntry->writeEntry($outputStream);
}
$centralDirectoryOffset = ftell($outputStream);
foreach ($memoryEntriesResult as $key => $outputEntry) {
if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
unset($memoryEntriesResult[$key]);
}
}
$centralDirectoryEntries = sizeof($memoryEntriesResult);
$this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
$outputStream,
$centralDirectoryEntries,
$centralDirectoryOffset
);
}
/**
* Writes a Central File Header record.
*
* @param resource $outputStream
* @param ZipEntry $entry
* @return bool false if and only if the record has been skipped,
* i.e. not written for some other reason than an I/O error.
*/
private function writeCentralFileHeader($outputStream, ZipEntry $entry)
{
$compressedSize = $entry->getCompressedSize();
$size = $entry->getSize();
// This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
// UNKNOWN!
if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
return false;
}
$extra = $entry->getExtra();
$extraSize = strlen($extra);
$commentLength = strlen($entry->getComment());
fwrite(
$outputStream,
pack(
'VvvvvVVVVvvvvvVV',
// central file header signature 4 bytes (0x02014b50)
self::CENTRAL_FILE_HEADER_SIG,
// version made by 2 bytes
($entry->getPlatform() << 8) | 63,
// version needed to extract 2 bytes
$entry->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$entry->getMethod(),
// last mod file datetime 4 bytes
$entry->getTime(),
// crc-32 4 bytes
$entry->getCrc(),
// compressed size 4 bytes
$entry->getCompressedSize(),
// uncompressed size 4 bytes
$entry->getSize(),
// file name length 2 bytes
strlen($entry->getName()),
// extra field length 2 bytes
$extraSize,
// file comment length 2 bytes
$commentLength,
// disk number start 2 bytes
0,
// internal file attributes 2 bytes
0,
// external file attributes 4 bytes
$entry->getExternalAttributes(),
// relative offset of local header 4 bytes
$entry->getOffset()
)
);
// file name (variable size)
fwrite($outputStream, $entry->getName());
if (0 < $extraSize) {
// extra field (variable size)
fwrite($outputStream, $extra);
}
if (0 < $commentLength) {
// file comment (variable size)
fwrite($outputStream, $entry->getComment());
}
return true;
}
public function release()
{
unset($this->entries);
unset($this->modifiedEntries);
}
function __destruct()
{
$this->release();
}
}

View File

@@ -0,0 +1,427 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Util\PackUtil;
/**
* Read End of Central Directory
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class EndOfCentralDirectory
{
/** Zip64 End Of Central Directory Record. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
/** Zip64 End Of Central Directory Locator. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
/** End Of Central Directory Record signature. */
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
/**
* The minimum length of the End Of Central Directory Record.
*
* end of central dir signature 4
* number of this disk 2
* number of the disk with the
* start of the central directory 2
* total number of entries in the
* central directory on this disk 2
* total number of entries in
* the central directory 2
* size of the central directory 4
* offset of start of central *
* directory with respect to *
* the starting disk number 4
* zipfile comment length 2
*/
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
/**
* The length of the Zip64 End Of Central Directory Locator.
* zip64 end of central dir locator
* signature 4
* number of the disk with the
* start of the zip64 end of
* central directory 4
* relative offset of the zip64
* end of central directory record 8
* total number of disks 4
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
/**
* The minimum length of the Zip64 End Of Central Directory Record.
*
* zip64 end of central dir
* signature 4
* size of zip64 end of central
* directory record 8
* version made by 2
* version needed to extract 2
* number of this disk 4
* number of the disk with the
* start of the central directory 4
* total number of entries in the
* central directory on this disk 8
* total number of entries in
* the central directory 8
* size of the central directory 8
* offset of start of central
* directory with respect to
* the starting disk number 8
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
/**
* @var string|null The archive comment.
*/
private $comment;
/**
* @var int The number of bytes in the preamble of this ZIP file.
*/
private $preamble;
/**
* @var int The number of bytes in the postamble of this ZIP file.
*/
private $postamble;
/**
* @var PositionMapper Maps offsets specified in the ZIP file to real offsets in the file.
*/
private $mapper;
/**
* @var int
*/
private $centralDirectoryEntriesSize;
/**
* @var bool
*/
private $zip64 = false;
/**
* @var string|null
*/
private $newComment;
/**
* @var bool
*/
private $modified;
/**
* EndOfCentralDirectory constructor.
*/
public function __construct()
{
$this->mapper = new PositionMapper();
}
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP file.
*
* @param resource $inputStream
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
*/
public function findCentralDirectory($inputStream)
{
// Search for End of central directory record.
$stats = fstat($inputStream);
$size = $stats['size'];
$max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1])
continue;
// number of this disk - 2 bytes
// number of the disk with the start of the
// central directory - 2 bytes
// total number of entries in the central
// directory on this disk - 2 bytes
// total number of entries in the central
// directory - 2 bytes
// size of the central directory - 4 bytes
// offset of start of central directory with
// respect to the starting disk number - 4 bytes
// ZIP file comment length - 2 bytes
$data = unpack(
'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
fread($inputStream, 18)
);
if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!"
);
}
// .ZIP file comment (variable size)
if (0 < $data['commentLength']) {
$this->comment = fread($inputStream, $data['commentLength']);
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $size - ftell($inputStream);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($inputStream) === $size ||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1]
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $data['cdSize'];
fseek($inputStream, $offset, SEEK_SET);
$offset -= $data['cdPos'];
if (0 !== $offset) {
$this->mapper = new OffsetPositionMapper($offset);
}
$this->centralDirectoryEntriesSize = $data['cdEntries'];
return;
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1];
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8));
// total number of disks 4 bytes
$totalDisks = unpack('V', fread($inputStream, 4))[1];
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1];
if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
}
// size of zip64 end of central
// directory record 8 bytes
// version made by 2 bytes
// version needed to extract 2 bytes
fseek($inputStream, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = unpack('V', fread($inputStream, 4))[1];
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = unpack('V', fread($inputStream, 4))[1];
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($inputStream, 8));
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
throw new ZipException("Total Number Of Entries In The Central Directory out of range!");
}
// size of the central directory 8 bytes
fseek($inputStream, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($inputStream, 8));
// zip64 extensible data sector (variable size)
fseek($inputStream, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
$this->centralDirectoryEntriesSize = $cdEntries;
$this->zip64 = true;
return;
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $size - $min;
$this->centralDirectoryEntriesSize = 0;
}
/**
* @return null|string
*/
public function getComment()
{
return $this->comment;
}
/**
* @return int
*/
public function getCentralDirectoryEntriesSize()
{
return $this->centralDirectoryEntriesSize;
}
/**
* @return bool
*/
public function isZip64()
{
return $this->zip64;
}
/**
* @return int
*/
public function getPreamble()
{
return $this->preamble;
}
/**
* @return int
*/
public function getPostamble()
{
return $this->postamble;
}
/**
* @return PositionMapper
*/
public function getMapper()
{
return $this->mapper;
}
/**
* @param int $preamble
*/
public function setPreamble($preamble)
{
$this->preamble = $preamble;
}
/**
* Set archive comment
* @param string|null $comment
* @throws InvalidArgumentException
*/
public function setComment($comment = null)
{
if (null !== $comment && strlen($comment) !== 0) {
$comment = (string)$comment;
$length = strlen($comment);
if (0x0000 > $length || $length > 0xffff) {
throw new InvalidArgumentException('Length comment out of range');
}
}
$this->modified = $comment !== $this->comment;
$this->newComment = $comment;
}
/**
* @return bool
*/
public function isModified()
{
return $this->modified;
}
/**
* Write end of central directory.
*
* @param resource $outputStream Output stream
* @param int $centralDirectoryEntries Size entries
* @param int $centralDirectoryOffset Offset central directory
*/
public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset)
{
$position = ftell($outputStream);
$centralDirectorySize = $position - $centralDirectoryOffset;
$centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff;
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries;
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
$zip64 // ZIP64 extensions?
= $centralDirectoryEntriesZip64
|| $centralDirectorySizeZip64
|| $centralDirectoryOffsetZip64;
if ($zip64) {
// relative offset of the zip64 end of central directory record
$zip64EndOfCentralDirectoryOffset = $position;
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
// size of zip64 end of central
// directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
// version made by 2 bytes
// version needed to extract 2 bytes
// due to potential use of BZIP2 compression
// number of this disk 4 bytes
// number of the disk with the
// start of the central directory 4 bytes
fwrite($outputStream, pack('vvVV', 63, 46, 0, 0));
// total number of entries in the
// central directory on this disk 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// total number of entries in the
// central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// size of the central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize));
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset));
// zip64 extensible data sector (variable size)
//
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
// relative offset of the zip64
// end of central directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
// total number of disks 4 bytes
fwrite($outputStream, pack('V', 1));
}
$comment = $this->modified ? $this->newComment : $this->comment;
$commentLength = strlen($comment);
fwrite(
$outputStream,
pack('VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
// number of this disk 2 bytes
0,
// number of the disk with the
// start of the central directory 2 bytes
0,
// total number of entries in the
// central directory on this disk 2 bytes
$centralDirectoryEntries16,
// total number of entries in
// the central directory 2 bytes
$centralDirectoryEntries16,
// size of the central directory 4 bytes
$centralDirectorySize32,
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
$centralDirectoryOffset32,
// .ZIP file comment length 2 bytes
$commentLength
)
);
if ($commentLength > 0) {
// .ZIP file comment (variable size)
fwrite($outputStream, $comment);
}
}
}

View File

@@ -0,0 +1,926 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\DefaultExtraField;
use PhpZip\Extra\ExtraField;
use PhpZip\Extra\ExtraFields;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\CentralDirectory;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\DateTimeConverter;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFile;
/**
* Abstract ZIP entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ZipAbstractEntry implements ZipEntry
{
/**
* @var CentralDirectory
*/
private $centralDirectory;
/**
* @var int Bit flags for init state.
*/
private $init;
/**
* @var string Entry name (filename in archive)
*/
private $name;
/**
* @var int Made by platform
*/
private $platform;
/**
* @var int
*/
private $versionNeededToExtract = 20;
/**
* @var int
*/
private $general;
/**
* @var int Compression method
*/
private $method;
/**
* @var int Dos time
*/
private $dosTime;
/**
* @var int Crc32
*/
private $crc;
/**
* @var int Compressed size
*/
private $compressedSize = self::UNKNOWN;
/**
* @var int Uncompressed size
*/
private $size = self::UNKNOWN;
/**
* @var int External attributes
*/
private $externalAttributes;
/**
* @var int Relative Offset Of Local File Header.
*/
private $offset = self::UNKNOWN;
/**
* The map of Extra Fields.
* Maps from Header ID [Integer] to Extra Field [ExtraField].
* Should be null or may be empty if no Extra Fields are used.
*
* @var ExtraFields
*/
private $fields;
/**
* @var string Comment field.
*/
private $comment;
/**
* @var string Entry password for read or write encryption data.
*/
private $password;
/**
* Encryption method.
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
* @var int
*/
private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL;
/**
* @var int
*/
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
/**
* @param int $mask
* @return bool
*/
private function isInit($mask)
{
return 0 !== ($this->init & $mask);
}
/**
* @param int $mask
* @param bool $init
*/
private function setInit($mask, $init)
{
if ($init) {
$this->init |= $mask;
} else {
$this->init &= ~$mask;
}
}
/**
* @return CentralDirectory
*/
public function getCentralDirectory()
{
return $this->centralDirectory;
}
/**
* @param CentralDirectory $centralDirectory
* @return ZipEntry
*/
public function setCentralDirectory(CentralDirectory $centralDirectory)
{
$this->centralDirectory = $centralDirectory;
return $this;
}
/**
* Returns the ZIP entry name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set entry name.
*
* @param string $name New entry name
* @return ZipEntry
* @throws ZipException
*/
public function setName($name)
{
$length = strlen($name);
if (0x0000 > $length || $length > 0xffff) {
throw new ZipException('Illegal zip entry name parameter');
}
$encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true);
$this->setGeneralPurposeBitFlag(self::GPBF_UTF8, $encoding === 'UTF-8');
$this->name = $name;
return $this;
}
/**
* @return int Get platform
*/
public function getPlatform()
{
return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN;
}
/**
* Set platform
*
* @param int $platform
* @return ZipEntry
* @throws ZipException
*/
public function setPlatform($platform)
{
$known = self::UNKNOWN !== $platform;
if ($known) {
if (0x00 > $platform || $platform > 0xff) {
throw new ZipException("Platform out of range");
}
$this->platform = $platform;
} else {
$this->platform = 0;
}
$this->setInit(self::BIT_PLATFORM, $known);
return $this;
}
/**
* Version needed to extract.
*
* @return int
*/
public function getVersionNeededToExtract()
{
return $this->versionNeededToExtract;
}
/**
* Set version needed to extract.
*
* @param int $version
* @return ZipEntry
*/
public function setVersionNeededToExtract($version)
{
$this->versionNeededToExtract = $version;
return $this;
}
/**
* @return bool
*/
public function isZip64ExtensionsRequired()
{
// Offset MUST be considered in decision about ZIP64 format - see
// description of Data Descriptor in ZIP File Format Specification!
return 0xffffffff <= $this->getCompressedSize()
|| 0xffffffff <= $this->getSize()
|| 0xffffffff <= $this->getOffset();
}
/**
* Returns the compressed size of this entry.
*
* @see int
*/
public function getCompressedSize()
{
return $this->compressedSize;
}
/**
* Sets the compressed size of this entry.
*
* @param int $compressedSize The Compressed Size.
* @return ZipEntry
* @throws ZipException
*/
public function setCompressedSize($compressedSize)
{
if (self::UNKNOWN != $compressedSize) {
if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) {
throw new ZipException("Compressed size out of range - " . $this->name);
}
}
$this->compressedSize = $compressedSize;
return $this;
}
/**
* Returns the uncompressed size of this entry.
*
* @see ZipEntry::setCompressedSize
*/
public function getSize()
{
return $this->size;
}
/**
* Sets the uncompressed size of this entry.
*
* @param int $size The (Uncompressed) Size.
* @return ZipEntry
* @throws ZipException
*/
public function setSize($size)
{
if (self::UNKNOWN != $size) {
if (0 > $size || $size > 0x7fffffffffffffff) {
throw new ZipException("Uncompressed Size out of range - " . $this->name);
}
}
$this->size = $size;
return $this;
}
/**
* Return relative Offset Of Local File Header.
*
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @param int $offset
* @return ZipEntry
* @throws ZipException
*/
public function setOffset($offset)
{
if (0 > $offset || $offset > 0x7fffffffffffffff) {
throw new ZipException("Offset out of range - " . $this->name);
}
$this->offset = $offset;
return $this;
}
/**
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @return bool
*/
public function isDirectory()
{
return $this->name[strlen($this->name) - 1] === '/';
}
/**
* Returns the General Purpose Bit Flags.
*
* @return bool
*/
public function getGeneralPurposeBitFlags()
{
return $this->general & 0xffff;
}
/**
* Sets the General Purpose Bit Flags.
*
* @var int general
* @return ZipEntry
* @throws ZipException
*/
public function setGeneralPurposeBitFlags($general)
{
if (0x0000 > $general || $general > 0xffff) {
throw new ZipException('general out of range');
}
$this->general = $general;
return $this;
}
/**
* Returns the indexed General Purpose Bit Flag.
*
* @param int $mask
* @return bool
*/
public function getGeneralPurposeBitFlag($mask)
{
return 0 !== ($this->general & $mask);
}
/**
* Sets the indexed General Purpose Bit Flag.
*
* @param int $mask
* @param bool $bit
* @return ZipEntry
*/
public function setGeneralPurposeBitFlag($mask, $bit)
{
if ($bit)
$this->general |= $mask;
else
$this->general &= ~$mask;
return $this;
}
/**
* Returns true if and only if this ZIP entry is encrypted.
*
* @return bool
*/
public function isEncrypted()
{
return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
}
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
*
* @return ZipEntry
*/
public function clearEncryption()
{
$this->setEncrypted(false);
if (null !== $this->fields) {
$field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
/**
* @var WinZipAesEntryExtraField $field
*/
$this->removeExtraField(WinZipAesEntryExtraField::getHeaderId());
}
if (self::METHOD_WINZIP_AES === $this->getMethod()) {
$this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod());
}
}
$this->password = null;
return $this;
}
/**
* Sets the encryption flag for this ZIP entry.
*
* @param bool $encrypted
* @return ZipEntry
*/
public function setEncrypted($encrypted)
{
$this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
return $this;
}
/**
* Returns the compression method for this entry.
*
* @return int
*/
public function getMethod()
{
return $this->isInit(self::BIT_METHOD) ? $this->method & 0xffff : self::UNKNOWN;
}
/**
* Sets the compression method for this entry.
*
* @param int $method
* @return ZipEntry
* @throws ZipException If method is not STORED, DEFLATED, BZIP2 or UNKNOWN.
*/
public function setMethod($method)
{
if (0x0000 > $method || $method > 0xffff) {
throw new ZipException('method out of range');
}
switch ($method) {
case self::METHOD_WINZIP_AES:
$this->method = $method;
$this->setInit(self::BIT_METHOD, true);
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
break;
case ZipFile::METHOD_STORED:
case ZipFile::METHOD_DEFLATED:
case ZipFile::METHOD_BZIP2:
$this->method = $method;
$this->setInit(self::BIT_METHOD, true);
break;
case self::UNKNOWN:
$this->method = ZipFile::METHOD_STORED;
$this->setInit(self::BIT_METHOD, false);
break;
default:
throw new ZipException($this->name . " (unsupported compression method $method)");
}
return $this;
}
/**
* Get Unix Timestamp
*
* @return int
*/
public function getTime()
{
if (!$this->isInit(self::BIT_DATE_TIME)) {
return self::UNKNOWN;
}
return DateTimeConverter::toUnixTimestamp($this->dosTime & 0xffffffff);
}
/**
* Set time from unix timestamp.
*
* @param int $unixTimestamp
* @return ZipEntry
*/
public function setTime($unixTimestamp)
{
$known = self::UNKNOWN != $unixTimestamp;
if ($known) {
$this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
} else {
$this->dosTime = 0;
}
$this->setInit(self::BIT_DATE_TIME, $known);
return $this;
}
/**
* Returns the external file attributes.
*
* @return int The external file attributes.
*/
public function getExternalAttributes()
{
if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) {
return $this->isDirectory() ? 0x10 : 0;
}
return $this->externalAttributes & 0xffffffff;
}
/**
* Sets the external file attributes.
*
* @param int $externalAttributes the external file attributes.
* @return ZipEntry
* @throws ZipException
*/
public function setExternalAttributes($externalAttributes)
{
$known = self::UNKNOWN != $externalAttributes;
if ($known) {
if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) {
throw new ZipException("external file attributes out of range - " . $this->name);
}
$this->externalAttributes = $externalAttributes;
} else {
$this->externalAttributes = 0;
}
$this->setInit(self::BIT_EXTERNAL_ATTR, $known);
return $this;
}
/**
* Return extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
*/
public function getExtraField($headerId)
{
return $this->fields === null ? null : $this->fields->get($headerId);
}
/**
* Add extra field.
*
* @param ExtraField $field
* @return ExtraField
* @throws ZipException
*/
public function addExtraField($field)
{
if (null === $field) {
throw new ZipException("extra field null");
}
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
return $this->fields->add($field);
}
/**
* Return exists extra field from header id.
*
* @param int $headerId
* @return bool
*/
public function hasExtraField($headerId)
{
return $this->fields === null ? false : $this->fields->has($headerId);
}
/**
* Remove extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
*/
public function removeExtraField($headerId)
{
return null !== $this->fields ? $this->fields->remove($headerId) : null;
}
/**
* Returns a protective copy of the serialized Extra Fields.
*
* @return string A new byte array holding the serialized Extra Fields.
* null is never returned.
*/
public function getExtra()
{
return $this->getExtraFields(false);
}
/**
* @param bool $zip64
* @return string
* @throws ZipException
*/
private function getExtraFields($zip64)
{
if ($zip64) {
$field = $this->composeZip64ExtraField();
if (null !== $field) {
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
$this->fields->add($field);
}
} else {
assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID));
}
return null === $this->fields ? null : $this->fields->getExtra();
}
/**
* Composes a ZIP64 Extended Information Extra Field from the properties
* of this entry.
* If no ZIP64 Extended Information Extra Field is required it is removed
* from the collection of Extra Fields.
*
* @return ExtraField|null
*/
private function composeZip64ExtraField()
{
$handle = fopen('php://memory', 'r+b');
// Write out Uncompressed Size.
$size = $this->getSize();
if (0xffffffff <= $size) {
fwrite($handle, PackUtil::packLongLE($size));
}
// Write out Compressed Size.
$compressedSize = $this->getCompressedSize();
if (0xffffffff <= $compressedSize) {
fwrite($handle, PackUtil::packLongLE($compressedSize));
}
// Write out Relative Header Offset.
$offset = $this->getOffset();
if (0xffffffff <= $offset) {
fwrite($handle, PackUtil::packLongLE($offset));
}
// Create ZIP64 Extended Information Extra Field from serialized data.
$field = null;
if (ftell($handle) > 0) {
$field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID);
$field->readFrom($handle, 0, ftell($handle));
} else {
$field = null;
}
return $field;
}
/**
* Sets the serialized Extra Fields by making a protective copy.
* Note that this method parses the serialized Extra Fields according to
* the ZIP File Format Specification and limits its size to 64 KB.
* Therefore, this property cannot not be used to hold arbitrary
* (application) data.
* Consider storing such data in a separate entry instead.
*
* @param string $data The byte array holding the serialized Extra Fields.
* @throws ZipException if the serialized Extra Fields exceed 64 KB
* @return ZipEntry
* or do not conform to the ZIP File Format Specification
*/
public function setExtra($data)
{
if (null !== $data) {
$length = strlen($data);
if (0x0000 > $length || $length > 0xffff) {
throw new ZipException("Extra Fields too large");
}
}
if (null === $data || strlen($data) <= 0) {
$this->fields = null;
} else {
$this->setExtraFields($data, false);
}
return $this;
}
/**
* @param string $data
* @param bool $zip64
*/
private function setExtraFields($data, $zip64)
{
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
$handle = fopen('php://memory', 'r+b');
fwrite($handle, $data);
rewind($handle);
$this->fields->readFrom($handle, 0, strlen($data));
$result = false;
if ($zip64) {
$result = $this->parseZip64ExtraField();
}
if ($result) {
$this->fields->remove(ExtraField::ZIP64_HEADER_ID);
if ($this->fields->size() <= 0) {
if (0 !== $this->fields->size()) {
$this->fields = null;
}
}
}
fclose($handle);
}
/**
* Parses the properties of this entry from the ZIP64 Extended Information
* Extra Field, if present.
* The ZIP64 Extended Information Extra Field is not removed.
*
* @return bool
* @throws ZipException
*/
private function parseZip64ExtraField()
{
if (null === $this->fields) {
return false;
}
$ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID);
if (null === $ef) {
return false;
}
$dataBlockHandle = $ef->getDataBlock();
$off = 0;
// Read in Uncompressed Size.
$size = $this->getSize();
if (0xffffffff <= $size) {
assert(0xffffffff === $size);
fseek($dataBlockHandle, $off);
$this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
$off += 8;
}
// Read in Compressed Size.
$compressedSize = $this->getCompressedSize();
if (0xffffffff <= $compressedSize) {
assert(0xffffffff === $compressedSize);
fseek($dataBlockHandle, $off);
$this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
$off += 8;
}
// Read in Relative Header Offset.
$offset = $this->getOffset();
if (0xffffffff <= $offset) {
assert(0xffffffff, $offset);
fseek($dataBlockHandle, $off);
$this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
//$off += 8;
}
fclose($dataBlockHandle);
return true;
}
/**
* Returns comment entry
*
* @return string
*/
public function getComment()
{
return null != $this->comment ? $this->comment : "";
}
/**
* Set entry comment.
*
* @param $comment
* @return ZipEntry
* @throws ZipException
*/
public function setComment($comment)
{
if (null !== $comment) {
$commentLength = strlen($comment);
if (0x0000 > $commentLength || $commentLength > 0xffff) {
throw new ZipException("Comment too long");
}
}
$encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true);
if ($encoding === 'UTF-8') {
$this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true);
}
$this->comment = $comment;
return $this;
}
/**
* @return bool
*/
public function isDataDescriptorRequired()
{
return self::UNKNOWN == ($this->getCrc() | $this->getCompressedSize() | $this->getSize());
}
/**
* Return crc32 content or 0 for WinZip AES v2
*
* @return int
*/
public function getCrc()
{
return $this->crc & 0xffffffff;
}
/**
* Set crc32 content.
*
* @param int $crc
* @return ZipEntry
* @throws ZipException
*/
public function setCrc($crc)
{
if (0x00000000 > $crc || $crc > 0xffffffff) {
throw new ZipException("CRC-32 out of range - " . $this->name);
}
$this->crc = $crc;
$this->setInit(self::BIT_CRC, true);
return $this;
}
/**
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Set password and encryption method from entry
*
* @param string $password
* @param null|int $encryptionMethod
* @return ZipEntry
*/
public function setPassword($password, $encryptionMethod = null)
{
$this->password = $password;
if ($encryptionMethod !== null) {
$this->setEncryptionMethod($encryptionMethod);
}
$this->setEncrypted(!empty($this->password));
return $this;
}
/**
* @return int
*/
public function getEncryptionMethod()
{
return $this->encryptionMethod;
}
/**
* @return int
*/
public function getCompressionLevel()
{
return $this->compressionLevel;
}
/**
* @param int $compressionLevel
* @return ZipEntry
* @throws InvalidArgumentException
*/
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
{
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
}
$this->compressionLevel = $compressionLevel;
return $this;
}
/**
* Set encryption method
*
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
*
* @param int $encryptionMethod
* @return ZipEntry
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod)
{
if (
ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod &&
ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod
) {
throw new ZipException('Invalid encryption method');
}
$this->encryptionMethod = $encryptionMethod;
$this->setEncrypted(true);
return $this;
}
/**
* Clone extra fields
*/
function __clone()
{
$this->fields = $this->fields !== null ? clone $this->fields : null;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* New zip entry from empty dir.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewEmptyDirEntry extends ZipNewEntry
{
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return null;
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFile;
/**
* Abstract class for new zip entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ZipNewEntry extends ZipAbstractEntry
{
/**
* Default compression level for bzip2
*/
const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
/**
* Version needed to extract.
*
* @return int
*/
public function getVersionNeededToExtract()
{
$method = $this->getMethod();
return self::METHOD_WINZIP_AES === $method ? 51 :
(ZipFile::METHOD_BZIP2 === $method ? 46 :
($this->isZip64ExtensionsRequired() ? 45 :
(ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10)
)
);
}
/**
* Write local file header, encryption header, file data and data descriptor to output stream.
*
* @param resource $outputStream
* @throws ZipException
*/
public function writeEntry($outputStream)
{
$nameLength = strlen($this->getName());
$size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment());
if (0xffff < $size) {
throw new ZipException($this->getName()
. " (the total size of "
. $size
. " bytes for the name, extra fields and comment exceeds the maximum size of "
. 0xffff . " bytes)");
}
if (self::UNKNOWN === $this->getPlatform()) {
$this->setPlatform(self::PLATFORM_UNIX);
}
if (self::UNKNOWN === $this->getTime()) {
$this->setTime(time());
}
$method = $this->getMethod();
if (self::UNKNOWN === $method) {
$this->setMethod($method = ZipFile::METHOD_DEFLATED);
}
$skipCrc = false;
$encrypted = $this->isEncrypted();
$dd = $this->isDataDescriptorRequired();
// Compose General Purpose Bit Flag.
// See appendix D of PKWARE's ZIP File Format Specification.
$utf8 = true;
$general = ($encrypted ? self::GPBF_ENCRYPTED : 0)
| ($dd ? self::GPBF_DATA_DESCRIPTOR : 0)
| ($utf8 ? self::GPBF_UTF8 : 0);
$entryContent = $this->getEntryContent();
$this->setSize(strlen($entryContent));
$this->setCrc(crc32($entryContent));
if ($encrypted && null === $this->getPassword()) {
throw new ZipException("Can not password from entry " . $this->getName());
}
if (
$encrypted &&
(
self::METHOD_WINZIP_AES === $method ||
$this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES
)
) {
$field = null;
$method = $this->getMethod();
$keyStrength = 256; // bits
$compressedSize = $this->getCompressedSize();
if (self::METHOD_WINZIP_AES === $method) {
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
$method = $field->getMethod();
if (self::UNKNOWN !== $compressedSize) {
$compressedSize -= $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
}
$this->setMethod($method);
}
}
if (null === $field) {
$field = new WinZipAesEntryExtraField();
}
$field->setKeyStrength($keyStrength);
$field->setMethod($method);
$size = $this->getSize();
if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
} else {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
$skipCrc = true;
}
$this->addExtraField($field);
if (self::UNKNOWN !== $compressedSize) {
$compressedSize += $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
$this->setCompressedSize($compressedSize);
}
if ($skipCrc) {
$this->setCrc(0);
}
}
switch ($method) {
case ZipFile::METHOD_STORED:
break;
case ZipFile::METHOD_DEFLATED:
$entryContent = gzdeflate($entryContent, $this->getCompressionLevel());
break;
case ZipFile::METHOD_BZIP2:
$compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
self::LEVEL_DEFAULT_BZIP2_COMPRESSION :
$this->getCompressionLevel();
$entryContent = bzcompress($entryContent, $compressionLevel);
if (is_int($entryContent)) {
throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
}
break;
default:
throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")");
}
if ($encrypted) {
if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) {
if ($skipCrc) {
$this->setCrc(0);
}
$this->setMethod(self::METHOD_WINZIP_AES);
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$winZipAesEngine = new WinZipAesEngine($this, $field);
$entryContent = $winZipAesEngine->encrypt($entryContent);
} elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
$entryContent = $zipCryptoEngine->encrypt($entryContent);
}
}
$compressedSize = strlen($entryContent);
$this->setCompressedSize($compressedSize);
$offset = ftell($outputStream);
// Commit changes.
$this->setGeneralPurposeBitFlags($general);
$this->setOffset($offset);
$extra = $this->getExtra();
// zip align
$padding = 0;
$zipAlign = $this->getCentralDirectory()->getZipAlign();
$extraLength = strlen($extra);
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
$padding =
(
$zipAlign -
(
$offset +
ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
$nameLength + $extraLength
) % $zipAlign
) % $zipAlign;
}
fwrite(
$outputStream,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
self::LOCAL_FILE_HEADER_SIG,
// version needed to extract 2 bytes
$this->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$general,
// compression method 2 bytes
$this->getMethod(),
// last mod file time 2 bytes
// last mod file date 2 bytes
$this->getTime(),
// crc-32 4 bytes
$dd ? 0 : $this->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $this->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $this->getSize(),
// file name length 2 bytes
$nameLength,
// extra field length 2 bytes
$extraLength + $padding
)
);
fwrite($outputStream, $this->getName());
if ($extraLength > 0) {
fwrite($outputStream, $extra);
}
if ($padding > 0) {
fwrite($outputStream, str_repeat(chr(0), $padding));
}
if ($entryContent !== null) {
fwrite($outputStream, $entryContent);
}
assert(self::UNKNOWN !== $this->getCrc());
assert(self::UNKNOWN !== $this->getSize());
if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
// data descriptor signature 4 bytes (0x08074b50)
// crc-32 4 bytes
fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc()));
// compressed size 4 or 8 bytes
// uncompressed size 4 or 8 bytes
if ($this->isZip64ExtensionsRequired()) {
fwrite($outputStream, PackUtil::packLongLE($compressedSize));
fwrite($outputStream, PackUtil::packLongLE($this->getSize()));
} else {
fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize()));
}
} elseif ($this->getCompressedSize() !== $compressedSize) {
throw new ZipException($this->getName()
. " (expected compressed entry size of "
. $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
/**
* New zip entry from stream.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewStreamEntry extends ZipNewEntry
{
/**
* @var resource
*/
private $stream;
/**
* ZipNewStreamEntry constructor.
* @param resource $stream
* @throws InvalidArgumentException
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new InvalidArgumentException('stream is not resource');
}
$this->stream = $stream;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return stream_get_contents($this->stream, -1, 0);
}
/**
* Release stream resource.
*/
function __destruct()
{
if ($this->stream !== null) {
fclose($this->stream);
$this->stream = null;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* New zip entry from string.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewStringEntry extends ZipNewEntry
{
/**
* @var string
*/
private $entryContent;
/**
* ZipNewStringEntry constructor.
* @param string $entryContent
*/
public function __construct($entryContent)
{
$this->entryContent = $entryContent;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return $this->entryContent;
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\CentralDirectory;
use PhpZip\Model\ZipEntry;
use PhpZip\ZipFile;
/**
* This class is used to represent a ZIP file entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipReadEntry extends ZipAbstractEntry
{
/**
* Max size cached content in memory.
*/
const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 3145728; // 3 mb
/**
* @var resource
*/
private $inputStream;
/**
* @var string
*/
private $charset;
/**
* @var string|resource Cached entry content.
*/
private $entryContent;
/**
* ZipFileEntry constructor.
* @param $inputStream
*/
public function __construct($inputStream)
{
$this->inputStream = $inputStream;
$this->readZipEntry($inputStream);
}
/**
* @param resource $inputStream
* @throws InvalidArgumentException
*/
private function readZipEntry($inputStream)
{
// central file header signature 4 bytes (0x02014b50)
$fileHeaderSig = unpack('V', fread($inputStream, 4))[1];
if (CentralDirectory::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) {
throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
}
// version made by 2 bytes
// version needed to extract 2 bytes
// general purpose bit flag 2 bytes
// compression method 2 bytes
// last mod file time 2 bytes
// last mod file date 2 bytes
// crc-32 4 bytes
// compressed size 4 bytes
// uncompressed size 4 bytes
// file name length 2 bytes
// extra field length 2 bytes
// file comment length 2 bytes
// disk number start 2 bytes
// internal file attributes 2 bytes
// external file attributes 4 bytes
// relative offset of local header 4 bytes
$data = unpack(
'vversionMadeBy/vversionNeededToExtract/vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
'VrawSize/vfileLength/vextraLength/vcommentLength/VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
fread($inputStream, 42)
);
$utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
if ($utf8) {
$this->charset = "UTF-8";
}
// See appendix D of PKWARE's ZIP File Format Specification.
$name = fread($inputStream, $data['fileLength']);
$this->setName($name);
$this->setVersionNeededToExtract($data['versionNeededToExtract']);
$this->setPlatform($data['versionMadeBy'] >> 8);
$this->setGeneralPurposeBitFlags($data['gpbf']);
$this->setMethod($data['rawMethod']);
$this->setTime($data['rawTime']);
$this->setCrc($data['rawCrc']);
$this->setCompressedSize($data['rawCompressedSize']);
$this->setSize($data['rawSize']);
$this->setExternalAttributes($data['rawExternalAttributes']);
$this->setOffset($data['lfhOff']); // must be unmapped!
if (0 < $data['extraLength']) {
$this->setExtra(fread($inputStream, $data['extraLength']));
}
if (0 < $data['commentLength']) {
$this->setComment(fread($inputStream, $data['commentLength']));
}
}
/**
* Returns an string content of the given entry.
*
* @return string
* @throws ZipException
*/
public function getEntryContent()
{
if ($this->entryContent === null) {
$isEncrypted = $this->isEncrypted();
$password = $this->getPassword();
if ($isEncrypted && empty($password)) {
throw new ZipException("Not set password");
}
$pos = $this->getOffset();
assert(self::UNKNOWN !== $pos);
$startPos = $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
fseek($this->inputStream, $startPos);
// local file header signature 4 bytes (0x04034b50)
if (self::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->inputStream, 4))[1]) {
throw new ZipException($this->getName() . " (expected Local File Header)");
}
fseek($this->inputStream, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
// file name length 2 bytes
// extra field length 2 bytes
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
$pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
assert(self::UNKNOWN !== $this->getCrc());
$method = $this->getMethod();
fseek($this->inputStream, $pos);
// Get raw entry content
$content = fread($this->inputStream, $this->getCompressedSize());
// Strong Encryption Specification - WinZip AES
if ($this->isEncrypted()) {
if (self::METHOD_WINZIP_AES === $method) {
$winZipAesEngine = new WinZipAesEngine($this);
$content = $winZipAesEngine->decrypt($content);
// Disable redundant CRC-32 check.
$isEncrypted = false;
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
} else {
// Traditional PKWARE Decryption
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
$content = $zipCryptoEngine->decrypt($content);
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
}
}
if ($isEncrypted) {
// Check CRC32 in the Local File Header or Data Descriptor.
$localCrc = null;
if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
// The CRC32 is in the Data Descriptor after the compressed size.
// Note the Data Descriptor's Signature is optional:
// All newer apps should write it (and so does TrueVFS),
// but older apps might not.
fseek($this->inputStream, $pos + $this->getCompressedSize());
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
}
} else {
fseek($this->inputStream, $startPos + 14);
// The CRC32 in the Local File Header.
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
}
if ($this->getCrc() !== $localCrc) {
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
}
}
switch ($method) {
case ZipFile::METHOD_STORED:
break;
case ZipFile::METHOD_DEFLATED:
$content = gzinflate($content);
break;
case ZipFile::METHOD_BZIP2:
if (!extension_loaded('bz2')) {
throw new ZipException('Extension bzip2 not install');
}
$content = bzdecompress($content);
break;
default:
throw new ZipUnsupportMethod($this->getName()
. " (compression method "
. $method
. " is not supported)");
}
if ($isEncrypted) {
$localCrc = crc32($content);
if ($this->getCrc() !== $localCrc) {
if ($this->isEncrypted()) {
throw new ZipCryptoException("Wrong password");
}
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
}
}
if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) {
$this->entryContent = $content;
} else {
$this->entryContent = fopen('php://temp', 'rb');
fwrite($this->entryContent, $content);
}
return $content;
}
if (is_resource($this->entryContent)) {
return stream_get_contents($this->entryContent, -1, 0);
}
return $this->entryContent;
}
/**
* Write local file header, encryption header, file data and data descriptor to output stream.
*
* @param resource $outputStream
*/
public function writeEntry($outputStream)
{
$pos = $this->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
$pos += ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS;
$this->setOffset(ftell($outputStream));
// zip align
$padding = 0;
$zipAlign = $this->getCentralDirectory()->getZipAlign();
$extra = $this->getExtra();
$extraLength = strlen($extra);
$nameLength = strlen($this->getName());
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
$padding =
(
$zipAlign -
($this->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength)
% $zipAlign
) % $zipAlign;
}
$dd = $this->isDataDescriptorRequired();
fwrite(
$outputStream,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
self::LOCAL_FILE_HEADER_SIG,
// version needed to extract 2 bytes
$this->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$this->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$this->getMethod(),
// last mod file time 2 bytes
// last mod file date 2 bytes
$this->getTime(),
// crc-32 4 bytes
$dd ? 0 : $this->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $this->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $this->getSize(),
$nameLength,
// extra field length 2 bytes
$extraLength + $padding
)
);
fwrite($outputStream, $this->getName());
if ($extraLength > 0) {
fwrite($outputStream, $extra);
}
if ($padding > 0) {
fwrite($outputStream, str_repeat(chr(0), $padding));
}
fseek($this->inputStream, $pos);
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
fseek($this->inputStream, $data['fileLength'] + $data['extraLength'], SEEK_CUR);
$length = $this->getCompressedSize();
if ($this->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
$length += 12;
if ($this->isZip64ExtensionsRequired()) {
$length += 8;
}
}
stream_copy_to_stream($this->inputStream, $outputStream, $length);
}
function __destruct()
{
if ($this->entryContent !== null && is_resource($this->entryContent)) {
fclose($this->entryContent);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField; use PhpZip\Extra\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField; use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil; use PhpZip\Util\FilesUtil;
use PhpZip\ZipFile;
/** /**
* Zip info * Zip info
@@ -85,7 +86,7 @@ class ZipInfo
]; ];
private static $valuesCompressionMethod = [ private static $valuesCompressionMethod = [
ZipEntry::METHOD_STORED => 'no compression', ZipFile::METHOD_STORED => 'no compression',
1 => 'shrink', 1 => 'shrink',
2 => 'reduce level 1', 2 => 'reduce level 1',
3 => 'reduce level 2', 3 => 'reduce level 2',
@@ -93,7 +94,7 @@ class ZipInfo
5 => 'reduce level 4', 5 => 'reduce level 4',
6 => 'implode', 6 => 'implode',
7 => 'reserved for Tokenizing compression algorithm', 7 => 'reserved for Tokenizing compression algorithm',
ZipEntry::METHOD_DEFLATED => 'deflate', ZipFile::METHOD_DEFLATED => 'deflate',
9 => 'deflate64', 9 => 'deflate64',
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
11 => 'reserved by PKWARE', 11 => 'reserved by PKWARE',
@@ -107,7 +108,7 @@ class ZipInfo
19 => 'IBM LZ77 z Architecture (PFS)', 19 => 'IBM LZ77 z Architecture (PFS)',
97 => 'WavPack', 97 => 'WavPack',
98 => 'PPMd version I, Rev 1', 98 => 'PPMd version I, Rev 1',
ZipEntry::WINZIP_AES => 'WinZip AES', ZipEntry::METHOD_WINZIP_AES => 'WinZip AES',
]; ];
/** /**
@@ -214,34 +215,36 @@ class ZipInfo
$this->platform = self::getPlatformName($entry); $this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract(); $this->version = $entry->getVersionNeededToExtract();
$attribs = str_repeat(" ", 12); $attributes = str_repeat(" ", 12);
$xattr = (($entry->getRawExternalAttributes() >> 16) & 0xFFFF); $externalAttributes = $entry->getExternalAttributes();
$xattr = (($externalAttributes >> 16) & 0xFFFF);
switch ($entry->getPlatform()) { switch ($entry->getPlatform()) {
case self::MADE_BY_MS_DOS: case self::MADE_BY_MS_DOS:
/** @noinspection PhpMissingBreakStatementInspection */
case self::MADE_BY_WINDOWS_NTFS: case self::MADE_BY_WINDOWS_NTFS:
if ($entry->getPlatform() != self::MADE_BY_MS_DOS || if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
($xattr & 0700) != ($xattr & 0700) !=
(0400 | (0400 |
(!($entry->getRawExternalAttributes() & 1) << 7) | (!($externalAttributes & 1) << 7) |
(($entry->getRawExternalAttributes() & 0x10) << 2)) (($externalAttributes & 0x10) << 2))
) { ) {
$xattr = $entry->getRawExternalAttributes() & 0xFF; $xattr = $externalAttributes & 0xFF;
$attribs = ".r.-... "; $attributes = ".r.-... ";
$attribs[2] = ($xattr & 0x01) ? '-' : 'w'; $attributes[2] = ($xattr & 0x01) ? '-' : 'w';
$attribs[5] = ($xattr & 0x02) ? 'h' : '-'; $attributes[5] = ($xattr & 0x02) ? 'h' : '-';
$attribs[6] = ($xattr & 0x04) ? 's' : '-'; $attributes[6] = ($xattr & 0x04) ? 's' : '-';
$attribs[4] = ($xattr & 0x20) ? 'a' : '-'; $attributes[4] = ($xattr & 0x20) ? 'a' : '-';
if ($xattr & 0x10) { if ($xattr & 0x10) {
$attribs[0] = 'd'; $attributes[0] = 'd';
$attribs[3] = 'x'; $attributes[3] = 'x';
} else } else
$attribs[0] = '-'; $attributes[0] = '-';
if ($xattr & 0x08) if ($xattr & 0x08)
$attribs[0] = 'V'; $attributes[0] = 'V';
else { else {
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) { if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
$attribs[3] = 'x'; $attributes[3] = 'x';
} }
} }
break; break;
@@ -250,51 +253,51 @@ class ZipInfo
default: /* assume Unix-like */ default: /* assume Unix-like */
switch ($xattr & self::UNX_IFMT) { switch ($xattr & self::UNX_IFMT) {
case self::UNX_IFDIR: case self::UNX_IFDIR:
$attribs[0] = 'd'; $attributes[0] = 'd';
break; break;
case self::UNX_IFREG: case self::UNX_IFREG:
$attribs[0] = '-'; $attributes[0] = '-';
break; break;
case self::UNX_IFLNK: case self::UNX_IFLNK:
$attribs[0] = 'l'; $attributes[0] = 'l';
break; break;
case self::UNX_IFBLK: case self::UNX_IFBLK:
$attribs[0] = 'b'; $attributes[0] = 'b';
break; break;
case self::UNX_IFCHR: case self::UNX_IFCHR:
$attribs[0] = 'c'; $attributes[0] = 'c';
break; break;
case self::UNX_IFIFO: case self::UNX_IFIFO:
$attribs[0] = 'p'; $attributes[0] = 'p';
break; break;
case self::UNX_IFSOCK: case self::UNX_IFSOCK:
$attribs[0] = 's'; $attributes[0] = 's';
break; break;
default: default:
$attribs[0] = '?'; $attributes[0] = '?';
break; break;
} }
$attribs[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-'; $attributes[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
$attribs[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-'; $attributes[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
$attribs[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-'; $attributes[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
$attribs[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-'; $attributes[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
$attribs[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-'; $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
$attribs[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-'; $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
if ($xattr & self::UNX_IXUSR) if ($xattr & self::UNX_IXUSR)
$attribs[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x'; $attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
else else
$attribs[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */ $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
if ($xattr & self::UNX_IXGRP) if ($xattr & self::UNX_IXGRP)
$attribs[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */ $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
else else
$attribs[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */ $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
if ($xattr & self::UNX_IXOTH) if ($xattr & self::UNX_IXOTH)
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */ $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
else else
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */ $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
} }
$this->attributes = trim($attribs); $this->attributes = trim($attributes);
} }
/** /**
@@ -305,7 +308,7 @@ class ZipInfo
{ {
$return = ''; $return = '';
if ($entry->isEncrypted()) { if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::WINZIP_AES) { if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
if ($field !== null) { if ($field !== null) {

View File

@@ -1,22 +0,0 @@
<?php
namespace PhpZip\Output;
/**
* Zip output entry for empty dir.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputEmptyDirEntry extends ZipOutputEntry
{
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return '';
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Model\ZipEntry;
/**
* Zip output Entry
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ZipOutputEntry
{
/**
* @var ZipEntry
*/
private $entry;
/**
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
if ($entry === null) {
throw new \RuntimeException('entry is null');
}
$this->entry = $entry;
}
/**
* Returns zip entry
*
* @return ZipEntry
*/
public function getEntry()
{
return $this->entry;
}
/**
* Returns entry data.
*
* @return string
*/
abstract public function getEntryContent();
}

View File

@@ -1,54 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Model\ZipEntry;
use RuntimeException;
/**
* Zip output entry for stream resource.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStreamEntry extends ZipOutputEntry
{
/**
* @var resource
*/
private $stream;
/**
* @param resource $stream
* @param ZipEntry $entry
*/
public function __construct($stream, ZipEntry $entry)
{
parent::__construct($entry);
if (!is_resource($stream)) {
throw new RuntimeException('stream is not resource');
}
$this->stream = $stream;
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
rewind($this->stream);
return stream_get_contents($this->stream);
}
/**
* Release stream resource.
*/
function __destruct()
{
if ($this->stream !== null) {
fclose($this->stream);
$this->stream = null;
}
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
/**
* Zip output entry for string data.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStringEntry extends ZipOutputEntry
{
/**
* Data content.
*
* @var string
*/
private $data;
/**
* @param string $data
* @param ZipEntry $entry
* @throws ZipException If data empty.
*/
public function __construct($data, ZipEntry $entry)
{
parent::__construct($entry);
$data = (string)$data;
if ($data === null) {
throw new ZipException("data is null");
}
$this->data = $data;
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return $this->data;
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
use PhpZip\ZipFile;
/**
* Zip output entry for input zip file.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputZipFileEntry extends ZipOutputEntry
{
/**
* Input zip file.
*
* @var ZipFile
*/
private $inputZipFile;
/**
* Input entry name.
*
* @var string
*/
private $inputEntryName;
/**
* ZipOutputZipFileEntry constructor.
* @param ZipFile $zipFile
* @param ZipEntry $zipEntry
* @throws ZipException If input zip file is null.
*/
public function __construct(ZipFile $zipFile, ZipEntry $zipEntry)
{
if ($zipFile === null) {
throw new ZipException('ZipFile is null');
}
parent::__construct(clone $zipEntry);
$this->inputZipFile = $zipFile;
$this->inputEntryName = $zipEntry->getName();
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return $this->inputZipFile->getEntryContent($this->inputEntryName);
}
}

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace PhpZip\Util; namespace PhpZip\Util;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
/** /**
@@ -14,7 +15,7 @@ class CryptoUtil
* *
* @param int $length * @param int $length
* @return string * @return string
* @throws ZipException * @throws RuntimeException
*/ */
public static final function randomBytes($length) public static final function randomBytes($length)
{ {
@@ -26,7 +27,7 @@ class CryptoUtil
} elseif (function_exists('mcrypt_create_iv')) { } elseif (function_exists('mcrypt_create_iv')) {
return mcrypt_create_iv($length); return mcrypt_create_iv($length);
} else { } else {
throw new ZipException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
} }
} }
} }

View File

@@ -1,115 +0,0 @@
<?php
namespace PhpZip;
/**
* Constants for ZIP files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipConstants
{
/** Local File Header signature. */
const LOCAL_FILE_HEADER_SIG = 0x04034B50;
/** Data Descriptor signature. */
const DATA_DESCRIPTOR_SIG = 0x08074B50;
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
/** Zip64 End Of Central Directory Record. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
/** Zip64 End Of Central Directory Locator. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
/** End Of Central Directory Record signature. */
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
/**
* The minimum length of the Local File Header record.
*
* local file header signature 4
* version needed to extract 2
* general purpose bit flag 2
* compression method 2
* last mod file time 2
* last mod file date 2
* crc-32 4
* compressed size 4
* uncompressed size 4
* file name length 2
* extra field length 2
*/
const LOCAL_FILE_HEADER_MIN_LEN = 30;
/**
* The minimum length of the End Of Central Directory Record.
*
* end of central dir signature 4
* number of this disk 2
* number of the disk with the
* start of the central directory 2
* total number of entries in the
* central directory on this disk 2
* total number of entries in
* the central directory 2
* size of the central directory 4
* offset of start of central *
* directory with respect to *
* the starting disk number 4
* zipfile comment length 2
*/
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
/**
* The length of the Zip64 End Of Central Directory Locator.
* zip64 end of central dir locator
* signature 4
* number of the disk with the
* start of the zip64 end of
* central directory 4
* relative offset of the zip64
* end of central directory record 8
* total number of disks 4
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
/**
* The minimum length of the Zip64 End Of Central Directory Record.
*
* zip64 end of central dir
* signature 4
* size of zip64 end of central
* directory record 8
* version made by 2
* version needed to extract 2
* number of this disk 4
* number of the disk with the
* start of the central directory 4
* total number of entries in the
* central directory on this disk 8
* total number of entries in
* the central directory 8
* size of the central directory 8
* offset of start of central
* directory with respect to
* the starting disk number 8
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
/**
* Local File Header signature 4
* Version Needed To Extract 2
* General Purpose Bit Flags 2
* Compression Method 2
* Last Mod File Time 2
* Last Mod File Date 2
* CRC-32 4
* Compressed Size 4
* Uncompressed Size 4
*/
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1378
tests/PhpZip/ZipFileTest.php Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<?php <?php
namespace PhpZip; namespace PhpZip;
use PhpZip\Model\EndOfCentralDirectory;
/** /**
* PHPUnit test case and helper methods. * PHPUnit test case and helper methods.
@@ -25,12 +26,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
$output = implode(PHP_EOL, $output); $output = implode(PHP_EOL, $output);
if ($password !== null && $returnCode === 81) { if ($password !== null && $returnCode === 81) {
if(`which 7z`){ if (`which 7z`) {
// WinZip 99-character limit // WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99); $password = substr($password, 0, 99);
$command = "7z t -p" . escapeshellarg($password). " " . escapeshellarg($filename); $command = "7z t -p" . escapeshellarg($password) . " " . escapeshellarg($filename);
exec($command, $output, $returnCode); exec($command, $output, $returnCode);
$output = implode(PHP_EOL, $output); $output = implode(PHP_EOL, $output);
@@ -38,14 +39,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
self::assertEquals($returnCode, 0); self::assertEquals($returnCode, 0);
self::assertNotContains(' Errors', $output); self::assertNotContains(' Errors', $output);
self::assertContains(' Ok', $output); self::assertContains(' Ok', $output);
} else {
fwrite(STDERR, 'Program unzip cannot support this function.' . PHP_EOL);
fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full' . PHP_EOL);
} }
else{ } else {
fwrite(STDERR, 'Program unzip cannot support this function.'.PHP_EOL); self::assertEquals($returnCode, 0, $output);
fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full'.PHP_EOL);
}
}
else {
self::assertEquals($returnCode, 0);
self::assertNotContains('incorrect password', $output); self::assertNotContains('incorrect password', $output);
self::assertContains(' OK', $output); self::assertContains(' OK', $output);
self::assertContains('No errors', $output); self::assertContains('No errors', $output);
@@ -67,18 +66,20 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
self::assertContains('Empty zipfile', $output); self::assertContains('Empty zipfile', $output);
} }
$actualEmptyZipData = pack('VVVVVv', ZipConstants::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0); $actualEmptyZipData = pack('VVVVVv', EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
self::assertEquals(file_get_contents($filename), $actualEmptyZipData); self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
} }
/** /**
* @param string $filename * @param string $filename
* @param bool $showErrors
* @return bool|null If null - can not install zipalign * @return bool|null If null - can not install zipalign
*/ */
public static function doZipAlignVerify($filename) public static function doZipAlignVerify($filename, $showErrors = false)
{ {
if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) { if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode); exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output));
return $returnCode === 0; return $returnCode === 0;
} else { } else {
fwrite(STDERR, 'Can not find program "zipalign" for test'); fwrite(STDERR, 'Can not find program "zipalign" for test');