1
0
mirror of https://github.com/Ne-Lexa/php-zip.git synced 2025-10-19 09:06:17 +02:00

Completely rewritten code.

Implement read-only zip file - class \PhpZip\ZipFile, create and update zip file - \PhpZip\ZipOutputFile.
Supports ZIP64 ext, Traditional PKWARE Encryption, WinZip AES Encryption.
This commit is contained in:
Ne-Lexa
2016-09-26 12:04:38 +03:00
parent 5c23e588ff
commit 560a94c910
48 changed files with 7785 additions and 2905 deletions

View File

@@ -0,0 +1,215 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
/**
* Traditional PKWARE Encryption Engine.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class TraditionalPkwareEncryptionEngine
{
/**
* Encryption header size
*/
const STD_DEC_HDR_SIZE = 12;
/**
* Crc table
*
* @var array
*/
private static $CRC_TABLE = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
];
/**
* Encryption keys
*
* @var array
*/
private $keys = [];
/**
* @var ZipEntry
*/
private $entry;
/**
* ZipCryptoEngine constructor.
*
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
$this->initKeys($entry->getPassword());
}
/**
* Initial keys
*
* @param string $password
*/
private function initKeys($password)
{
$this->keys[0] = 305419896;
$this->keys[1] = 591751049;
$this->keys[2] = 878082192;
foreach (unpack('C*', $password) as $b) {
$this->updateKeys($b);
}
}
/**
* Update keys.
*
* @param string $charAt
*/
private function updateKeys($charAt)
{
$this->keys[0] = self::crc32($this->keys[0], $charAt);
$this->keys[1] = ($this->keys[1] + ($this->keys[0] & 0xff)) & 4294967295;
$this->keys[1] = ($this->keys[1] * 134775813 + 1) & 4294967295;
$this->keys[2] = self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff);
}
/**
* Update crc.
*
* @param int $oldCrc
* @param string $charAt
* @return int
*/
private function crc32($oldCrc, $charAt)
{
return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff];
}
/**
* @param string $content
* @return string
* @throws ZipAuthenticationException
*/
public function decrypt($content)
{
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
foreach ($headerBytes as &$byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte);
}
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// compare against the file type from extended local headers
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
} else {
// compare against the CRC otherwise
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
}
if ($headerBytes[11] !== $checkByte) {
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName());
}
$outputContent = "";
foreach (unpack('C*', substr($content, self::STD_DEC_HDR_SIZE)) as $val) {
$val = ($val ^ $this->decryptByte()) & 0xff;
$this->updateKeys($val);
$outputContent .= pack('c', $val);
}
return $outputContent;
}
/**
* Decrypt byte.
*
* @return int
*/
private function decryptByte()
{
$temp = $this->keys[2] | 2;
return (($temp * ($temp ^ 1)) >> 8) & 0xffffff;
}
/**
* Encryption data
*
* @param string $data
* @param int $crc
* @return string
*/
public function encrypt($data, $crc)
{
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted.
$this->initKeys($this->entry->getPassword());
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
$headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
$headerBytes = $this->encryptData($headerBytes);
return $headerBytes . $this->encryptData($data);
}
/**
* @param string $content
* @return string
*/
private function encryptData($content)
{
if ($content === null) {
throw new \RuntimeException();
}
$buff = '';
foreach (unpack('C*', $content) as $val) {
$buff .= pack('c', $this->encryptByte($val));
}
return $buff;
}
/**
* @param int $byte
* @return int
*/
protected function encryptByte($byte)
{
$tempVal = $byte ^ $this->decryptByte() & 0xff;
$this->updateKeys($byte);
return $tempVal;
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
/**
* WinZip Aes Encryption Engine.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEngine
{
/**
* The block size of the Advanced Encryption Specification (AES) Algorithm
* in bits (AES_BLOCK_SIZE_BITS).
*/
const AES_BLOCK_SIZE_BITS = 128;
const PWD_VERIFIER_BITS = 16;
/**
* The iteration count for the derived keys of the cipher, KLAC and MAC.
*/
const ITERATION_COUNT = 1000;
/**
* @var ZipEntry
*/
private $entry;
/**
* WinZipAesEngine constructor.
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
}
/**
* Decrypt from stream resource.
*
* @param resource $stream Input stream resource
* @return string
* @throws ZipAuthenticationException
* @throws ZipCryptoException
*/
public function decrypt($stream)
{
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
if (null === $field) {
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
}
$pos = ftell($stream);
// Get key strength.
$keyStrengthBits = $field->getKeyStrength();
$keyStrengthBytes = $keyStrengthBits / 8;
$salt = fread($stream, $keyStrengthBytes / 2);
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8);
$sha1Size = 20;
// Init start, end and size of encrypted data.
$endPos = $pos + $this->entry->getCompressedSize();
$start = ftell($stream);
$footerSize = $sha1Size / 2;
$end = $endPos - $footerSize;
$size = $end - $start;
if (0 > $size) {
throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)");
}
// Load authentication code.
fseek($stream, $end, SEEK_SET);
$authenticationCode = fread($stream, $footerSize);
if (ftell($stream) !== $endPos) {
// This should never happen unless someone is writing to the
// end of the file concurrently!
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
}
do {
assert($this->entry->getPassword() !== null);
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
// Here comes the strange part about WinZip AES encryption:
// Its unorthodox use of the Password-Based Key Derivation
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
// Yes, the password verifier is only a 16 bit value.
// So we must use the MAC for password verification, too.
$keyParam = hash_pbkdf2("sha1", $this->entry->getPassword(), $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
$key = substr($keyParam, 0, $keyStrengthBytes);
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
// Verify password.
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
$content = stream_get_contents($stream, $size, $start);
$mac = hash_hmac('sha1', $content, $sha1MacParam, true);
if ($authenticationCode !== substr($mac, 0, 10)) {
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)");
}
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
}
/**
* Decryption or encryption AES-CTR with Segment Integer Count (SIC).
*
* @param bool $encrypted If true encryption else decryption
* @param string $str Data
* @param string $key Key
* @param string $iv IV
* @return string
*/
private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv)
{
$numOfBlocks = ceil(strlen($str) / 16);
$ctrStr = '';
for ($i = 0; $i < $numOfBlocks; ++$i) {
for ($j = 0; $j < 16; ++$j) {
$n = ord($iv[$j]);
if (++$n === 0x100) {
// overflow, set this one to 0, increment next
$iv[$j] = chr(0);
} else {
// no overflow, just write incremented number back and abort
$iv[$j] = chr($n);
break;
}
}
$data = substr($str, $i * 16, 16);
$ctrStr .= $encrypted ?
self::encryptCtr($data, $key, $iv) :
self::decryptCtr($data, $key, $iv);
}
return $ctrStr;
}
/**
* Encrypt AES-CTR.
*
* @param string $data Raw data
* @param string $key Aes key
* @param string $iv Aes IV
* @return string Encrypted data
*/
private static function encryptCtr($data, $key, $iv)
{
if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8;
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) {
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
}
}
/**
* Decrypt AES-CTR.
*
* @param string $data Encrypted data
* @param string $key Aes key
* @param string $iv Aes IV
* @return string Raw data
*/
private static function decryptCtr($data, $key, $iv)
{
if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8;
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) {
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
}
}
/**
* Encryption string.
*
* @param string $content
* @return string
*/
public function encrypt($content)
{
// Init key strength.
$password = $this->entry->getPassword();
assert($password !== null);
$keyStrengthBytes = 32;
$keyStrengthBits = $keyStrengthBytes * 8;
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
$salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
$sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
// Can you believe they "forgot" the nonce in the CTR mode IV?! :-(
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
$key = substr($keyParam, 0, $keyStrengthBytes);
$content = self::aesCtrSegmentIntegerCounter(true, $content, $key, $iv);
$mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
return ($salt .
substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) .
$content .
substr($mac, 0, 10)
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown to indicate a CRC32 mismatch between the declared value in the
* Central File Header and the Data Descriptor or between the declared value
* and the computed value from the decompressed data.
*
* The exception's detail message is the name of the ZIP entry.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class Crc32Exception extends ZipException
{
/**
* Expected crc.
*
* @var int
*/
private $expectedCrc;
/**
* Actual crc.
*
* @var int
*/
private $actualCrc;
/**
* Crc32Exception constructor.
*
* @param string $name
* @param int $expected
* @param int $actual
*/
public function __construct($name, $expected, $actual)
{
parent::__construct($name
. " (expected CRC32 value 0x"
. dechex($expected)
. ", but is actually 0x"
. dechex($actual)
. ")");
assert($expected != $actual);
$this->expectedCrc = $expected;
$this->actualCrc = $actual;
}
/**
* Returns expected crc.
*
* @return int
*/
public function getExpectedCrc()
{
return $this->expectedCrc;
}
/**
* Returns actual crc.
*
* @return int
*/
public function getActualCrc()
{
return $this->actualCrc;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown to indicate that a method has been passed an illegal or
* inappropriate argument.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class IllegalArgumentException extends ZipException
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown to indicate that an authenticated ZIP entry has been tampered with.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipAuthenticationException extends ZipCryptoException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if there is an issue when reading or writing an encrypted ZIP file
* or entry.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipCryptoException extends ZipException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Signals that a Zip exception of some sort has occurred.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
* @see \Exception
*/
class ZipException extends \Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if entry not found.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
* @see \Exception
*/
class ZipNotFoundEntry extends ZipException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if entry unsupport compression method.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
* @see \Exception
*/
class ZipUnsupportMethod extends ZipException
{
}

View File

@@ -0,0 +1,98 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Default implementation for an Extra Field in a Local or Central Header of a
* ZIP file.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class DefaultExtraField extends ExtraField
{
/**
* @var int
*/
private static $headerId;
/**
* @var string
*/
private $data;
/**
* Constructs a new Extra Field.
*
* @param int $headerId an unsigned short integer (two bytes) indicating the
* type of the Extra Field.
* @throws ZipException
*/
public function __construct($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
self::$headerId = $headerId;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return self::$headerId & 0xffff;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return null !== $this->data ? strlen($this->data) : 0;
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if ($size > 0) {
fseek($handle, $off, SEEK_SET);
$this->data = fread($handle, $size);
}
}
/**
* @param resource $handle
* @param int $off
*/
public function writeTo($handle, $off)
{
if (null !== $this->data) {
fseek($handle, $off, SEEK_SET);
fwrite($handle, $this->data);
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Abstract base class for an Extra Field in a Local or Central Header of a
* ZIP archive.
* It defines the common properties of all Extra Fields and how to
* serialize/deserialize them to/from byte arrays.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ExtraField implements ExtraFieldHeader
{
/** The Header ID for a ZIP64 Extended Information Extra Field. */
const ZIP64_HEADER_ID = 0x0001;
/**
* @var array|null
*/
private static $registry;
/**
* A static factory method which creates a new Extra Field based on the
* given Header ID.
* The returned Extra Field still requires proper initialization, for
* example by calling ExtraField::readFrom.
*
* @param int $headerId An unsigned short integer (two bytes) which indicates
* the type of the returned Extra Field.
* @return ExtraField A new Extra Field or null if not support header id.
* @throws ZipException If headerId is out of range.
*/
public static function create($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
/**
* @var ExtraField $extraField
*/
if (isset(self::getRegistry()[$headerId])) {
$extraClassName = self::getRegistry()[$headerId];
$extraField = new $extraClassName;
if ($headerId !== $extraField::getHeaderId()) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
$extraField = new DefaultExtraField($headerId);
}
return $extraField;
}
/**
* Registered extra field classes.
*
* @return array|null
*/
private static function getRegistry()
{
if (self::$registry === null) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = '\PhpZip\Extra\WinZipAesEntryExtraField';
self::$registry[NtfsExtraField::getHeaderId()] = '\PhpZip\Extra\NtfsExtraField';
}
return self::$registry;
}
/**
* Returns a protective copy of the Data Block.
*
* @return resource
* @throws ZipException If size data block out of range.
*/
public function getDataBlock()
{
$size = $this->getDataSize();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size data block out of range.');
}
$fp = fopen('php://temp', 'r+b');
if (0 === $size) return $fp;
$this->writeTo($fp, 0);
rewind($fp);
return $fp;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
abstract public function getDataSize();
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
abstract public function writeTo($handle, $off);
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
*/
abstract public function readFrom($handle, $off, $size);
}

View File

@@ -0,0 +1,21 @@
<?php
namespace PhpZip\Extra;
/**
* Interface ExtraFieldHeader
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ExtraFieldHeader
{
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId();
}

View File

@@ -0,0 +1,213 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Represents a collection of Extra Fields as they may
* be present at several locations in ZIP files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ExtraFields
{
/**
* The map of Extra Fields.
* Maps from Header ID to Extra Field.
* Must not be null, but may be empty if no Extra Fields are used.
* The map is sorted by Header IDs in ascending order.
*
* @var ExtraField[]
*/
private $extra = [];
/**
* Returns the number of Extra Fields in this collection.
*
* @return int
*/
public function size()
{
return sizeof($this->extra);
}
/**
* Returns the Extra Field with the given Header ID or null
* if no such Extra Field exists.
*
* @param int $headerId The requested Header ID.
* @return ExtraField The Extra Field with the given Header ID or
* if no such Extra Field exists.
* @throws ZipException If headerId is out of range.
*/
public function get($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
if (isset($this->extra[$headerId])) {
return $this->extra[$headerId];
}
return null;
}
/**
* Stores the given Extra Field in this collection.
*
* @param ExtraField $extraField The Extra Field to store in this collection.
* @return ExtraField The Extra Field previously associated with the Header ID of
* of the given Extra Field or null if no such Extra Field existed.
* @throws ZipException If headerId is out of range.
*/
public function add(ExtraField $extraField)
{
$headerId = $extraField::getHeaderId();
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
$this->extra[$headerId] = $extraField;
return $extraField;
}
/**
* Returns Extra Field exists
*
* @param int $headerId The requested Header ID.
* @return bool
*/
public function has($headerId)
{
return isset($this->extra[$headerId]);
}
/**
* Removes the Extra Field with the given Header ID.
*
* @param int $headerId The requested Header ID.
* @return ExtraField The Extra Field with the given Header ID or null
* if no such Extra Field exists.
* @throws ZipException If headerId is out of range or extra field not found.
*/
public function remove($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
if (isset($this->extra[$headerId])) {
$ef = $this->extra[$headerId];
unset($this->extra[$headerId]);
return $ef;
}
throw new ZipException('ExtraField not found');
}
/**
* Returns a protective copy of the Extra Fields.
* null is never returned.
*
* @return string
* @throws ZipException If size out of range
*/
public function getExtra()
{
$size = $this->getExtraLength();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if (0 === $size) return '';
$fp = fopen('php://temp', 'r+b');
$this->writeTo($fp, 0);
rewind($fp);
$content = stream_get_contents($fp);
fclose($fp);
return $content;
}
/**
* Returns the number of bytes required to hold the Extra Fields.
*
* @return int The length of the Extra Fields in bytes. May be 0.
* @see #getExtra
*/
public function getExtraLength()
{
if (empty($this->extra)) {
return 0;
}
$length = 0;
/**
* @var ExtraField $extraField
*/
foreach ($this->extra as $extraField) {
$length += 4 + $extraField->getDataSize();
}
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
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset
* @param int $size Size
* @throws ZipException If size out of range
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
$map = [];
if (null !== $handle && 0 < $size) {
$end = $off + $size;
while ($off < $end) {
fseek($handle, $off, SEEK_SET);
$unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
$off += 4;
$extraField = ExtraField::create($unpack['headerId']);
$extraField->readFrom($handle, $off, $unpack['dataSize']);
$off += $unpack['dataSize'];
$map[$unpack['headerId']] = $extraField;
}
assert($off === $end);
}
$this->extra = $map;
}
/**
* If clone extra fields.
*/
function __clone()
{
foreach ($this->extra as $k => $v) {
$this->extra[$k] = clone $v;
}
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
use PhpZip\Util\PackUtil;
/**
* NTFS Extra Field
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class NtfsExtraField extends ExtraField
{
/**
* Modify time
*
* @var int Unix Timestamp
*/
private $mtime;
/**
* Access Time
*
* @var int Unix Timestamp
*/
private $atime;
/**
* Create Time
*
* @var int Unix Time
*/
private $ctime;
/**
* @var string
*/
private $rawData = "";
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return 0x000a;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return 8 * 4 + strlen($this->rawData);
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException If size out of range
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if ($size > 0) {
$off += 4;
fseek($handle, $off, SEEK_SET);
$unpack = unpack('vtag/vsizeAttr', fread($handle, 4));
if ($unpack['sizeAttr'] === 24) {
$tagData = fread($handle, $unpack['sizeAttr']);
$this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600;
$this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600;
$this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600;
}
$off += $unpack['sizeAttr'];
if ($size > $off) {
$this->rawData .= fread($handle, $size - $off);
}
}
}
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
public function writeTo($handle, $off)
{
if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) {
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData)));
$mtimeLong = ($this->mtime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($mtimeLong));
$atimeLong = ($this->atime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($atimeLong));
$ctimeLong = ($this->ctime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($ctimeLong));
if (!empty($this->rawData)) {
fwrite($handle, $this->rawData);
}
}
}
/**
* @return int
*/
public function getMtime()
{
return $this->mtime;
}
/**
* @param int $mtime
*/
public function setMtime($mtime)
{
$this->mtime = (int)$mtime;
}
/**
* @return int
*/
public function getAtime()
{
return $this->atime;
}
/**
* @param int $atime
*/
public function setAtime($atime)
{
$this->atime = (int)$atime;
}
/**
* @return int
*/
public function getCtime()
{
return $this->ctime;
}
/**
* @param int $ctime
*/
public function setCtime($ctime)
{
$this->ctime = (int)$ctime;
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* WinZip AES Extra Field.
*
* @see http://www.winzip.com/win/en/aes_info.htm AES Encryption Information: Encryption Specification AE-1 and AE-2 (WinZip Computing, S.L.)
* @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers (WinZip Computing, S.L.)
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEntryExtraField extends ExtraField
{
const DATA_SIZE = 7;
const VENDOR_ID = 17729; // 'A' | ('E' << 8);
/**
* Entries of this type <em>do</em> include the standard ZIP CRC-32 value.
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
*/
const VV_AE_1 = 1;
/**
* Entries of this type do <em>not</em> include the standard ZIP CRC-32 value.
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
*/
const VV_AE_2 = 2;
const KEY_STRENGTH_128BIT = 128;
const KEY_STRENGTH_192BIT = 192;
const KEY_STRENGTH_256BIT = 256;
private static $keyStrengths = [
self::KEY_STRENGTH_128BIT => 0x01,
self::KEY_STRENGTH_192BIT => 0x02,
self::KEY_STRENGTH_256BIT => 0x03
];
/**
* Vendor version.
*
* @var int
*/
private $vendorVersion = self::VV_AE_1;
/**
* Encryption strength.
*
* @var int
*/
private $encryptionStrength = self::KEY_STRENGTH_256BIT;
/**
* Zip compression method.
*
* @var int
*/
private $method;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return 0x9901;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return self::DATA_SIZE;
}
/**
* Returns the vendor version.
*
* @see WinZipAesEntryExtraField::VV_AE_1
* @see WinZipAesEntryExtraField::VV_AE_2
*/
public function getVendorVersion()
{
return $this->vendorVersion & 0xffff;
}
/**
* Sets the vendor version.
*
* @see WinZipAesEntryExtraField::VV_AE_1
* @see WinZipAesEntryExtraField::VV_AE_2
* @param int $vendorVersion the vendor version.
* @throws ZipException Unsupport vendor version.
*/
public function setVendorVersion($vendorVersion)
{
if ($vendorVersion < self::VV_AE_1 || self::VV_AE_2 < $vendorVersion) {
throw new ZipException($vendorVersion);
}
$this->vendorVersion = $vendorVersion;
}
/**
* Returns vendor id.
*
* @return int
*/
public function getVendorId()
{
return self::VENDOR_ID;
}
/**
* @return bool|int
*/
public function getKeyStrength()
{
return self::keyStrength($this->encryptionStrength);
}
/**
* @param int $encryptionStrength Encryption strength as bits.
* @return int
* @throws ZipException If unsupport encryption strength.
*/
public static function keyStrength($encryptionStrength)
{
$flipKeyStrength = array_flip(self::$keyStrengths);
if (!isset($flipKeyStrength[$encryptionStrength])) {
throw new ZipException("Unsupport encryption strength " . $encryptionStrength);
}
return $flipKeyStrength[$encryptionStrength];
}
/**
* Returns compression method.
*
* @return int
*/
public function getMethod()
{
return $this->method & 0xffff;
}
/**
* Sets compression method.
*
* @param int $compressionMethod Compression method
* @throws ZipException Compression method out of range.
*/
public function setMethod($compressionMethod)
{
if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) {
throw new ZipException('Compression method out of range');
}
$this->method = $compressionMethod;
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException
*/
public function readFrom($handle, $off, $size)
{
if (self::DATA_SIZE != $size)
throw new ZipException();
fseek($handle, $off, SEEK_SET);
/**
* @var int $vendorVersion
* @var int $vendorId
* @var int $keyStrength
* @var int $method
*/
$unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7));
extract($unpack);
$this->setVendorVersion($vendorVersion);
if (self::VENDOR_ID != $vendorId) {
throw new ZipException();
}
$this->setKeyStrength(self::keyStrength($keyStrength)); // checked
$this->setMethod($method);
}
/**
* Set key strength.
*
* @param int $keyStrength
*/
public function setKeyStrength($keyStrength)
{
$this->encryptionStrength = self::encryptionStrength($keyStrength);
}
/**
* Returns encryption strength.
*
* @param int $keyStrength Key strength in bits.
* @return int
*/
public static function encryptionStrength($keyStrength)
{
return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT];
}
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
public function writeTo($handle, $off)
{
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method));
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace PhpZip\Mapper;
/**
* Adds a offset value to the given position.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class OffsetPositionMapper extends PositionMapper
{
/**
* @var int
*/
private $offset;
/**
* @param int $offset
*/
public function __construct($offset)
{
$this->offset = $offset;
}
/**
* @param int $position
* @return int
*/
public function map($position)
{
return parent::map($position) + $this->offset;
}
/**
* @param int $position
* @return int
*/
public function unmap($position)
{
return parent::unmap($position) - $this->offset;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace PhpZip\Mapper;
/**
* Maps a given position.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class PositionMapper
{
/**
* @param int $position
* @return int
*/
public function map($position)
{
return $position;
}
/**
* @param int $position
* @return int
*/
public function unmap($position)
{
return $position;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
<?php
namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil;
/**
* Zip info
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipInfo
{
// made by constants
const MADE_BY_MS_DOS = 0;
const MADE_BY_AMIGA = 1;
const MADE_BY_OPEN_VMS = 2;
const MADE_BY_UNIX = 3;
const MADE_BY_VM_CMS = 4;
const MADE_BY_ATARI = 5;
const MADE_BY_OS_2 = 6;
const MADE_BY_MACINTOSH = 7;
const MADE_BY_Z_SYSTEM = 8;
const MADE_BY_CP_M = 9;
const MADE_BY_WINDOWS_NTFS = 10;
const MADE_BY_MVS = 11;
const MADE_BY_VSE = 12;
const MADE_BY_ACORN_RISC = 13;
const MADE_BY_VFAT = 14;
const MADE_BY_ALTERNATE_MVS = 15;
const MADE_BY_BEOS = 16;
const MADE_BY_TANDEM = 17;
const MADE_BY_OS_400 = 18;
const MADE_BY_OS_X = 19;
const MADE_BY_UNKNOWN = 20;
private static $valuesMadeBy = [
self::MADE_BY_MS_DOS => 'FAT',
self::MADE_BY_AMIGA => 'Amiga',
self::MADE_BY_OPEN_VMS => 'OpenVMS',
self::MADE_BY_UNIX => 'UNIX',
self::MADE_BY_VM_CMS => 'VM/CMS',
self::MADE_BY_ATARI => 'Atari ST',
self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.',
self::MADE_BY_MACINTOSH => 'Macintosh',
self::MADE_BY_Z_SYSTEM => 'Z-System',
self::MADE_BY_CP_M => 'CP/M',
self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS',
self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)',
self::MADE_BY_VSE => 'VSE',
self::MADE_BY_ACORN_RISC => 'Acorn Risc',
self::MADE_BY_VFAT => 'VFAT',
self::MADE_BY_ALTERNATE_MVS => 'Alternate MVS',
self::MADE_BY_BEOS => 'BeOS',
self::MADE_BY_TANDEM => 'Tandem',
self::MADE_BY_OS_400 => 'OS/400',
self::MADE_BY_OS_X => 'Mac OS X',
];
private static $valuesCompressionMethod = [
ZipEntry::METHOD_STORED => 'no compression',
1 => 'shrink',
2 => 'reduce level 1',
3 => 'reduce level 2',
4 => 'reduce level 3',
5 => 'reduce level 4',
6 => 'implode',
7 => 'reserved for Tokenizing compression algorithm',
ZipEntry::METHOD_DEFLATED => 'deflate',
9 => 'deflate64',
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
11 => 'reserved by PKWARE',
12 => 'bzip2',
13 => 'reserved by PKWARE',
14 => 'LZMA (EFS)',
15 => 'reserved by PKWARE',
16 => 'reserved by PKWARE',
17 => 'reserved by PKWARE',
18 => 'IBM TERSE',
19 => 'IBM LZ77 z Architecture (PFS)',
97 => 'WavPack',
98 => 'PPMd version I, Rev 1',
ZipEntry::WINZIP_AES => 'WinZip AES',
];
/**
* @var string
*/
private $path;
/**
* @var bool
*/
private $folder;
/**
* @var int
*/
private $size;
/**
* @var int
*/
private $compressedSize;
/**
* @var int
*/
private $mtime;
/**
* @var int|null
*/
private $ctime;
/**
* @var int|null
*/
private $atime;
/**
* @var bool
*/
private $encrypted;
/**
* @var string|null
*/
private $comment;
/**
* @var int
*/
private $crc;
/**
* @var string
*/
private $method;
/**
* @var string
*/
private $platform;
/**
* @var int
*/
private $version;
/**
* ZipInfo constructor.
*
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$mtime = $entry->getTime();
$atime = null;
$ctime = null;
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
if ($field !== null && $field instanceof NtfsExtraField) {
/**
* @var NtfsExtraField $field
*/
$atime = $field->getAtime();
$ctime = $field->getCtime();
}
$this->path = $entry->getName();
$this->folder = $entry->isDirectory();
$this->size = $entry->getSize();
$this->compressedSize = $entry->getCompressedSize();
$this->mtime = $mtime;
$this->ctime = $ctime;
$this->atime = $atime;
$this->encrypted = $entry->isEncrypted();
$this->comment = $entry->getComment();
$this->crc = $entry->getCrc();
$this->method = self::getMethodName($entry);
$this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract();
}
/**
* @param ZipEntry $entry
* @return string
*/
public static function getMethodName(ZipEntry $entry)
{
$return = '';
if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
if ($field !== null) {
/**
* @var WinZipAesEntryExtraField $field
*/
$return .= '-' . $field->getKeyStrength();
if (isset(self::$valuesCompressionMethod[$field->getMethod()])) {
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$field->getMethod()]);
}
}
} else {
$return .= 'ZipCrypto';
if (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
}
}
} elseif (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
} else {
$return = 'unknown';
}
return $return;
}
/**
* @param ZipEntry $entry
* @return string
*/
public static function getPlatformName(ZipEntry $entry)
{
if (isset(self::$valuesMadeBy[$entry->getPlatform()])) {
return self::$valuesMadeBy[$entry->getPlatform()];
} else {
return 'unknown';
}
}
/**
* @return array
*/
public function toArray()
{
return [
'path' => $this->getPath(),
'folder' => $this->isFolder(),
'size' => $this->getSize(),
'compressed_size' => $this->getCompressedSize(),
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'encrypted' => $this->isEncrypted(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method' => $this->getMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion()
];
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return boolean
*/
public function isFolder()
{
return $this->folder;
}
/**
* @return int
*/
public function getSize()
{
return $this->size;
}
/**
* @return int
*/
public function getCompressedSize()
{
return $this->compressedSize;
}
/**
* @return int
*/
public function getMtime()
{
return $this->mtime;
}
/**
* @return int|null
*/
public function getCtime()
{
return $this->ctime;
}
/**
* @return int|null
*/
public function getAtime()
{
return $this->atime;
}
/**
* @return boolean
*/
public function isEncrypted()
{
return $this->encrypted;
}
/**
* @return null|string
*/
public function getComment()
{
return $this->comment;
}
/**
* @return int
*/
public function getCrc()
{
return $this->crc;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* @return string
*/
public function getPlatform()
{
return $this->platform;
}
/**
* @return int
*/
public function getVersion()
{
return $this->version;
}
/**
* @return string
*/
function __toString()
{
return 'ZipInfo {'
. 'Path="' . $this->getPath() . '", '
. ($this->isFolder() ? 'Folder, ' : '')
. 'Size=' . FilesUtil::humanSize($this->getSize())
. ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize())
. ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', '
. ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '')
. ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '')
. ($this->isEncrypted() ? 'Encrypted, ' : '')
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
. 'Method="' . $this->getMethod() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';
}
}

View File

@@ -0,0 +1,22 @@
<?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

@@ -0,0 +1,46 @@
<?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

@@ -0,0 +1,54 @@
<?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

@@ -0,0 +1,46 @@
<?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

@@ -0,0 +1,56 @@
<?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

@@ -0,0 +1,32 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Crypto Utils
*/
class CryptoUtil
{
/**
* Returns random bytes.
*
* @param int $length
* @return string
* @throws ZipException
*/
public static final function randomBytes($length)
{
$length = (int)$length;
if (function_exists('random_bytes')) {
return random_bytes($length);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
return openssl_random_pseudo_bytes($length);
} elseif (function_exists('mcrypt_create_iv')) {
return mcrypt_create_iv($length);
} else {
throw new ZipException('Extension openssl or mcrypt not loaded');
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Convert unix timestamp values to DOS date/time values and vice versa.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class DateTimeConverter
{
/**
* Smallest supported DOS date/time value in a ZIP file,
* which is January 1st, 1980 AD 00:00:00 local time.
*/
const MIN_DOS_TIME = 0x210000; // (1 << 21) | (1 << 16)
/**
* Largest supported DOS date/time value in a ZIP file,
* which is December 31st, 2107 AD 23:59:58 local time.
*/
const MAX_DOS_TIME = 0xff9fbf7d; // ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 1);
/**
* Convert a 32 bit integer DOS date/time value to a UNIX timestamp value.
*
* @param int $dosTime Dos date/time
* @return int Unix timestamp
*/
public static function toUnixTimestamp($dosTime)
{
if (self::MIN_DOS_TIME > $dosTime) {
$dosTime = self::MIN_DOS_TIME;
} elseif (self::MAX_DOS_TIME < $dosTime) {
$dosTime = self::MAX_DOS_TIME;
}
return mktime(
($dosTime >> 11) & 0x1f, // hour
($dosTime >> 5) & 0x3f, // minute
2 * ($dosTime & 0x1f), // second
($dosTime >> 21) & 0x0f, // month
($dosTime >> 16) & 0x1f, // day
1980 + (($dosTime >> 25) & 0x7f) // year
);
}
/**
* Converts a UNIX timestamp value to a DOS date/time value.
*
* @param int $unixTimestamp The number of seconds since midnight, January 1st,
* 1970 AD UTC.
* @return int A DOS date/time value reflecting the local time zone and
* rounded down to even seconds
* and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME.
* @throws ZipException If unix timestamp is negative.
*/
public static function toDosTime($unixTimestamp)
{
if (0 > $unixTimestamp) {
throw new ZipException("Negative unix timestamp: " . $unixTimestamp);
}
$date = getdate($unixTimestamp);
if ($date['year'] < 1980) {
return self::MIN_DOS_TIME;
}
$date['year'] -= 1980;
return ($date['year'] << 25 | $date['mon'] << 21 |
$date['mday'] << 16 | $date['hours'] << 11 |
$date['minutes'] << 5 | $date['seconds'] >> 1);
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace PhpZip\Util;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
/**
* Files util.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class FilesUtil
{
/**
* Is empty directory
*
* @param string $dir Directory
* @return bool
*/
public static function isEmptyDir($dir)
{
if (!is_readable($dir)) {
return false;
}
return count(scandir($dir)) === 2;
}
/**
* Remove recursive directory.
*
* @param string $dir Directory path.
*/
public static function removeDir($dir)
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileInfo) {
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
$function($fileInfo->getRealPath());
}
rmdir($dir);
}
/**
* Convert glob pattern to regex pattern.
*
* @param string $globPattern
* @return string
*/
public static function convertGlobToRegEx($globPattern)
{
// Remove beginning and ending * globs because they're useless
$globPattern = trim($globPattern, '*');
$escaping = false;
$inCurrent = 0;
$chars = str_split($globPattern);
$regexPattern = '';
foreach ($chars AS $currentChar) {
switch ($currentChar) {
case '*':
$regexPattern .= ($escaping ? "\\*" : '.*');
$escaping = false;
break;
case '?':
$regexPattern .= ($escaping ? "\\?" : '.');
$escaping = false;
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
$regexPattern .= '\\' . $currentChar;
$escaping = false;
break;
case '\\':
if ($escaping) {
$regexPattern .= "\\\\";
$escaping = false;
} else {
$escaping = true;
}
break;
case '{':
if ($escaping) {
$regexPattern .= "\\{";
} else {
$regexPattern = '(';
$inCurrent++;
}
$escaping = false;
break;
case '}':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= ')';
$inCurrent--;
} else if ($escaping)
$regexPattern = "\\}";
else
$regexPattern = "}";
$escaping = false;
break;
case ',':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= '|';
} else if ($escaping)
$regexPattern .= "\\,";
else
$regexPattern = ",";
break;
default:
$escaping = false;
$regexPattern .= $currentChar;
}
}
return $regexPattern;
}
/**
* Search files.
*
* @param string $inputDir
* @param bool $recursive
* @param array $ignoreFiles
* @return array Searched file list
*/
public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = [])
{
$directoryIterator = $recursive ?
new \RecursiveDirectoryIterator($inputDir) :
new \DirectoryIterator($inputDir);
if (!empty($ignoreFiles)) {
$directoryIterator = $recursive ?
new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles) :
new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
}
$iterator = $recursive ?
new \RecursiveIteratorIterator($directoryIterator) :
new \IteratorIterator($directoryIterator);
$fileList = [];
foreach ($iterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Search files from glob pattern.
*
* @param string $globPattern
* @param int $flags
* @param bool $recursive
* @return array Searched file list
*/
public static function globFileSearch($globPattern, $flags = 0, $recursive = true)
{
$flags = (int)$flags;
$recursive = (bool)$recursive;
$files = glob($globPattern, $flags);
if (!$recursive) {
return $files;
}
foreach (glob(dirname($globPattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive));
}
return $files;
}
/**
* Search files from regex pattern.
*
* @param string $folder
* @param string $pattern
* @param bool $recursive
* @return array Searched file list
*/
public static function regexFileSearch($folder, $pattern, $recursive = true)
{
$directoryIterator = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder);
$iterator = $recursive ? new \RecursiveIteratorIterator($directoryIterator) : new \IteratorIterator($directoryIterator);
$regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
$fileList = [];
foreach ($regexIterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Convert bytes to human size.
*
* @param int $size Size bytes
* @param string|null $unit Unit support 'GB', 'MB', 'KB'
* @return string
*/
public static function humanSize($size, $unit = null)
{
if (($unit === null && $size >= 1 << 30) || $unit === "GB")
return number_format($size / (1 << 30), 2) . "GB";
if (($unit === null && $size >= 1 << 20) || $unit === "MB")
return number_format($size / (1 << 20), 2) . "MB";
if (($unit === null && $size >= 1 << 10) || $unit === "KB")
return number_format($size / (1 << 10), 2) . "KB";
return number_format($size) . " bytes";
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil;
/**
* Iterator for ignore files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class IgnoreFilesFilterIterator extends \FilterIterator
{
/**
* Ignore list files
*
* @var array
*/
private $ignoreFiles = ['..'];
/**
* @param \Iterator $iterator
* @param array $ignoreFiles
*/
public function __construct(\Iterator $iterator, array $ignoreFiles)
{
parent::__construct($iterator);
$this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles);
}
/**
* Check whether the current element of the iterator is acceptable
* @link http://php.net/manual/en/filteriterator.accept.php
* @return bool true if the current element is acceptable, otherwise false.
* @since 5.1.0
*/
public function accept()
{
/**
* @var \SplFileInfo $fileInfo
*/
$fileInfo = $this->current();
$pathname = str_replace('\\', '/', $fileInfo->getPathname());
foreach ($this->ignoreFiles as $ignoreFile) {
// handler dir and sub dir
if ($fileInfo->isDir()
&& $ignoreFile[strlen($ignoreFile) - 1] === '/'
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
) {
return false;
}
// handler filename
if (StringUtil::endsWith($pathname, $ignoreFile)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil;
/**
* Recursive iterator for ignore files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
{
/**
* Ignore list files
*
* @var array
*/
private $ignoreFiles = ['..'];
/**
* @param \RecursiveIterator $iterator
* @param array $ignoreFiles
*/
public function __construct(\RecursiveIterator $iterator, array $ignoreFiles)
{
parent::__construct($iterator);
$this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles);
}
/**
* Check whether the current element of the iterator is acceptable
* @link http://php.net/manual/en/filteriterator.accept.php
* @return bool true if the current element is acceptable, otherwise false.
* @since 5.1.0
*/
public function accept()
{
/**
* @var \SplFileInfo $fileInfo
*/
$fileInfo = $this->current();
$pathname = str_replace('\\', '/', $fileInfo->getPathname());
foreach ($this->ignoreFiles as $ignoreFile) {
// handler dir and sub dir
if ($fileInfo->isDir()
&& $ignoreFile[strlen($ignoreFile) - 1] === '/'
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
) {
return false;
}
// handler filename
if (StringUtil::endsWith($pathname, $ignoreFile)) {
return false;
}
}
return true;
}
/**
* @return IgnoreFilesRecursiveFilterIterator
*/
public function getChildren()
{
return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Pack util
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class PackUtil
{
/**
* @param int|string $longValue
* @return string
*/
public static function packLongLE($longValue)
{
// TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 0) {return pack("P", $longValue);}
$left = 0xffffffff00000000;
$right = 0x00000000ffffffff;
$r = ($longValue & $left) >> 32;
$l = $longValue & $right;
return pack('VV', $l, $r);
}
/**
* @param string|int $value
* @return int
* @throws ZipException
*/
public static function unpackLongLE($value)
{
// TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 0){ return current(unpack('P', $value)); }
$unpack = unpack('Va/Vb', $value);
return $unpack['a'] + ($unpack['b'] << 32);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace PhpZip\Util;
/**
* String Util
*/
class StringUtil
{
/**
* @param string $haystack
* @param string $needle
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false;
}
/**
* @param string $haystack
* @param string $needle
* @return bool
*/
public static function endsWith($haystack, $needle)
{
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
&& strpos($haystack, $needle, $temp) !== false);
}
}

115
src/PhpZip/ZipConstants.php Normal file
View File

@@ -0,0 +1,115 @@
<?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;
}

908
src/PhpZip/ZipFile.php Normal file
View File

@@ -0,0 +1,908 @@
<?php
namespace PhpZip;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\IllegalArgumentException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipInfo;
use PhpZip\Util\PackUtil;
/**
* This class is able to open the .ZIP file in read mode and extract files from it.
*
* Implemented support traditional PKWARE encryption and WinZip AES encryption.
* Implemented support ZIP64.
* Implemented support skip a preamble like the one found in self extracting archives.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
{
/**
* Input seekable stream resource.
*
* @var resource
*/
private $inputStream;
/**
* The total number of bytes in the ZIP archive.
*
* @var int
*/
private $length;
/**
* The charset to use for entry names and comments.
*
* @var string
*/
private $charset;
/**
* The number of bytes in the preamble of this ZIP file.
*
* @var int
*/
private $preamble;
/**
* The number of bytes in the postamble of this ZIP file.
*
* @var int
*/
private $postamble;
/**
* Maps entry names to zip entries.
*
* @var ZipEntry[]
*/
private $entries;
/**
* The file comment.
*
* @var string
*/
private $comment;
/**
* Maps offsets specified in the ZIP file to real offsets in the file.
*
* @var PositionMapper
*/
private $mapper;
/**
* Private ZipFile constructor.
*
* @see ZipFile::openFromFile()
* @see ZipFile::openFromString()
* @see ZipFile::openFromStream()
*/
private function __construct()
{
$this->mapper = new PositionMapper();
$this->charset = "UTF-8";
}
/**
* Open zip archive from file
*
* @param string $filename
* @return ZipFile
* @throws IllegalArgumentException if file doesn't exists.
* @throws ZipException if can't open file.
*/
public static function openFromFile($filename)
{
if (!file_exists($filename)) {
throw new IllegalArgumentException("File $filename can't exists.");
}
if (!($handle = fopen($filename, 'rb'))) {
throw new ZipException("File $filename can't open.");
}
$zipFile = self::openFromStream($handle);
$zipFile->length = filesize($filename);
return $zipFile;
}
/**
* Open zip archive from stream resource
*
* @param resource $handle
* @return ZipFile
* @throws IllegalArgumentException Invalid stream resource
* or resource cannot seekable stream
*/
public static function openFromStream($handle)
{
if (!is_resource($handle)) {
throw new IllegalArgumentException("Invalid stream resource.");
}
$meta = stream_get_meta_data($handle);
if (!$meta['seekable']) {
throw new IllegalArgumentException("Resource cannot seekable stream.");
}
$zipFile = new self();
$stats = fstat($handle);
if (isset($stats['size'])) {
$zipFile->length = $stats['size'];
}
$zipFile->checkZipFileSignature($handle);
$numEntries = $zipFile->findCentralDirectory($handle);
$zipFile->mountCentralDirectory($handle, $numEntries);
if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) {
assert(0 === $numEntries);
$zipFile->checkZipFileSignature($handle);
}
assert(null !== $handle);
assert(null !== $zipFile->charset);
assert(null !== $zipFile->entries);
assert(null !== $zipFile->mapper);
$zipFile->inputStream = $handle;
// Do NOT close stream!
return $zipFile;
}
/**
* Check zip file signature
*
* @param resource $handle
* @throws ZipException if this not .ZIP file.
*/
private function checkZipFileSignature($handle)
{
rewind($handle);
$signature = current(unpack('V', fread($handle, 4)));
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
) {
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
}
}
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP file.
*
* @param resource $handle
* @return int
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
*/
private function findCentralDirectory($handle)
{
// Search for End of central directory record.
$max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($handle, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4))))
continue;
// Process End Of Central Directory Record.
$data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4);
/**
* @var int $diskNo number of this disk - 2 bytes
* @var int $cdDiskNo number of the disk with the start of the
* central directory - 2 bytes
* @var int $cdEntriesDisk total number of entries in the central
* directory on this disk - 2 bytes
* @var int $cdEntries total number of entries in the central
* directory - 2 bytes
* @var int $cdSize size of the central directory - 4 bytes
* @var int $cdPos offset of start of central directory with
* respect to the starting disk number - 4 bytes
* @var int $commentLen ZIP file comment length - 2 bytes
*/
$unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data);
extract($unpack);
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!"
);
}
// .ZIP file comment (variable size)
if (0 < $commentLen) {
$this->comment = fread($handle, $commentLen);
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $this->length - ftell($handle);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($handle) === $this->length ||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4)))
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $cdSize;
fseek($handle, $offset, SEEK_SET);
$offset -= $cdPos;
if (0 !== $offset) {
$this->mapper = new OffsetPositionMapper($offset);
}
return (int)$cdEntries;
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4)));
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8));
// total number of disks 4 bytes
$totalDisks = current(unpack('V', fread($handle, 4)));
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4)));
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($handle, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = current(unpack('V', fread($handle, 4)));
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = current(unpack('V', fread($handle, 4)));
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($handle, 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
//$cdSize = self::getLongLE($channel);
fseek($handle, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($handle, 8));
// zip64 extensible data sector (variable size)
fseek($handle, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
return (int)$cdEntries;
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $this->length - $min;
return 0;
}
/**
* 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 $handle Input channel.
* @param int $numEntries Size zip entries.
* @throws ZipException
*/
private function mountCentralDirectory($handle, $numEntries)
{
$numEntries = (int)$numEntries;
$entries = [];
for (; ; $numEntries--) {
// central file header signature 4 bytes (0x02014b50)
if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) {
break;
}
// version made by 2 bytes
$versionMadeBy = current(unpack('v', fread($handle, 2)));
// version needed to extract 2 bytes
fseek($handle, 2, SEEK_CUR);
$unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26));
// disk number start 2 bytes
// internal file attributes 2 bytes
fseek($handle, 4, SEEK_CUR);
// external file attributes 4 bytes
// relative offset of local header 4 bytes
$unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8));
$utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8);
if ($utf8) {
$this->charset = "UTF-8";
}
// See appendix D of PKWARE's ZIP File Format Specification.
$name = fread($handle, $unpack['fileLen']);
$entry = new ZipEntry($name, $handle);
$entry->setRawPlatform($versionMadeBy >> 8);
$entry->setGeneralPurposeBitFlags($unpack['gpbf']);
$entry->setRawMethod($unpack['rawMethod']);
$entry->setRawTime($unpack['rawTime']);
$entry->setRawCrc($unpack['rawCrc']);
$entry->setRawCompressedSize($unpack['rawCompressedSize']);
$entry->setRawSize($unpack['rawSize']);
$entry->setRawExternalAttributes($unpack2['rawExternalAttributes']);
$entry->setRawOffset($unpack2['lfhOff']); // must be unmapped!
if (0 < $unpack['extraLen']) {
$entry->setRawExtraFields(fread($handle, $unpack['extraLen']));
}
if (0 < $unpack['commentLen']) {
$entry->setComment(fread($handle, $unpack['commentLen']));
}
unset($unpack, $unpack2);
// 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->mapper->map($entry->getOffset());
if ($lfhOff < $this->preamble) {
$this->preamble = $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;
}
/**
* Open zip archive from raw string data.
*
* @param string $data
* @return ZipFile
* @throws IllegalArgumentException if data not available.
* @throws ZipException if can't open temp stream.
*/
public static function openFromString($data)
{
if (empty($data)) {
throw new IllegalArgumentException("Data not available");
}
if (!($handle = fopen('php://temp', 'r+b'))) {
throw new ZipException("Can't open temp stream.");
}
fwrite($handle, $data);
rewind($handle);
$zipFile = self::openFromStream($handle);
$zipFile->length = strlen($data);
return $zipFile;
}
/**
* Returns the number of entries in this ZIP file.
*
* @return int
*/
public function count()
{
return sizeof($this->entries);
}
/**
* Returns the list files.
*
* @return string[]
*/
public function getListFiles()
{
return array_keys($this->entries);
}
/**
* @api
* @return ZipEntry[]
*/
public function getRawEntries()
{
return $this->entries;
}
/**
* Checks whether a entry exists
*
* @param string $entryName
* @return bool
*/
public function hasEntry($entryName)
{
return isset($this->entries[$entryName]);
}
/**
* Check whether the directory entry.
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @param string $entryName
* @return bool
* @throws ZipNotFoundEntry
*/
public function isDirectory($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
return $this->entries[$entryName]->isDirectory();
}
/**
* Set password to all encrypted entries.
*
* @param string $password Password
*/
public function setPassword($password)
{
foreach ($this->entries as $entry) {
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
}
/**
* Set password to concrete zip entry.
*
* @param string $entryName Zip entry name
* @param string $password Password
* @throws ZipNotFoundEntry if don't exist zip entry.
*/
public function setEntryPassword($entryName, $password)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
/**
* Returns the file comment.
*
* @return string The file comment.
*/
public function getComment()
{
return null === $this->comment ? '' : $this->decode($this->comment);
}
/**
* Decode charset entry name.
*
* @param string $text
* @return string
*/
private function decode($text)
{
$inCharset = mb_detect_encoding($text, mb_detect_order(), true);
if ($inCharset === $this->charset) return $text;
return iconv($inCharset, $this->charset, $text);
}
/**
* Returns entry comment.
*
* @param string $entryName
* @return string
* @throws ZipNotFoundEntry
*/
public function getEntryComment($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
return $this->entries[$entryName]->getComment();
}
/**
* Returns the name of the character set which is effectively used for
* decoding entry names and the file comment.
*
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* Returns the file length of this ZIP file in bytes.
*
* @return int
*/
public function length()
{
return $this->length;
}
/**
* Get info by entry.
*
* @param string|ZipEntry $entryName
* @return ZipInfo
* @throws ZipNotFoundEntry
*/
public function getEntryInfo($entryName)
{
if ($entryName instanceof ZipEntry) {
$entryName = $entryName->getName();
}
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
return new ZipInfo($entry);
}
/**
* Get info by all entries.
*
* @return ZipInfo[]
*/
public function getAllInfo()
{
return array_map([$this, 'getEntryInfo'], $this->entries);
}
/**
* Extract the archive contents
*
* Extract the complete archive or the given files to the specified destination.
*
* @param string $destination Location where to extract the files.
* @param array $entries The entries to extract. It accepts
* either a single entry name or an array of names.
* @return bool
* @throws ZipException
*/
public function extractTo($destination, $entries = null)
{
if ($this->entries === null) {
throw new ZipException("Zip entries not initial");
}
if (!file_exists($destination)) {
throw new ZipException("Destination " . $destination . " not found");
}
if (!is_dir($destination)) {
throw new ZipException("Destination is not directory");
}
if (!is_writable($destination)) {
throw new ZipException("Destination is not writable directory");
}
/**
* @var ZipEntry[] $zipEntries
*/
if (!empty($entries)) {
if (is_string($entries)) {
$entries = (array)$entries;
}
if (is_array($entries)) {
$flipEntries = array_flip($entries);
$zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) {
/**
* @var ZipEntry $zipEntry
*/
return isset($flipEntries[$zipEntry->getName()]);
});
}
} else {
$zipEntries = $this->entries;
}
$extract = 0;
foreach ($zipEntries AS $entry) {
$file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
if ($entry->isDirectory()) {
if (!is_dir($file)) {
if (!mkdir($file, 0755, true)) {
throw new ZipException("Can not create dir " . $file);
}
chmod($file, 0755);
touch($file, $entry->getTime());
}
continue;
}
$dir = dirname($file);
if (!file_exists($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new ZipException("Can not create dir " . $dir);
}
chmod($dir, 0755);
touch($file, $entry->getTime());
}
if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) {
return false;
}
touch($file, $entry->getTime());
$extract++;
}
return $extract > 0;
}
/**
* Returns an string content of the given entry.
*
* @param string $entryName
* @return string|null
* @throws ZipException
*/
public function getEntryContent($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
$pos = $entry->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$startPos = $pos = $this->mapper->map($pos);
fseek($this->inputStream, $pos, SEEK_SET);
$localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4)));
if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) {
throw new ZipException($entry->getName() . " (expected Local File Header)");
}
fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET);
$unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4));
$pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen'];
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
$check = $entry->isEncrypted();
$method = $entry->getMethod();
$password = $entry->getPassword();
if ($entry->isEncrypted() && empty($password)) {
throw new ZipException("Not set password");
}
// Strong Encryption Specification - WinZip AES
if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) {
fseek($this->inputStream, $pos, SEEK_SET);
$winZipAesEngine = new WinZipAesEngine($entry);
$content = $winZipAesEngine->decrypt($this->inputStream);
// Disable redundant CRC-32 check.
$check = false;
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES);
} else {
// Get raw entry content
$content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos);
// Traditional PKWARE Decryption
if ($entry->isEncrypted()) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$content = $zipCryptoEngine->decrypt($content);
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
}
}
if ($check) {
// Check CRC32 in the Local File Header or Data Descriptor.
$localCrc = null;
if ($entry->getGeneralPurposeBitFlag(ZipEntry::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 + $entry->getCompressedSize(), SEEK_SET);
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
}
} else {
fseek($this->inputStream, $startPos + 14, SEEK_SET);
// The CRC32 in the Local File Header.
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
}
if ($entry->getCrc() !== $localCrc) {
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
switch ($method) {
case ZipEntry::METHOD_STORED:
break;
case ZipEntry::METHOD_DEFLATED:
$content = gzinflate($content);
break;
case ZipEntry::METHOD_BZIP2:
if (!extension_loaded('bz2')) {
throw new ZipException('Extension bzip2 not install');
}
$content = bzdecompress($content);
break;
default:
throw new ZipUnsupportMethod($entry->getName()
. " (compression method "
. $method
. " is not supported)");
}
if ($check) {
$localCrc = crc32($content);
if ($entry->getCrc() !== $localCrc) {
if ($entry->isEncrypted()) {
throw new ZipCryptoException("Wrong password");
}
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
return $content;
}
/**
* Release all resources
*/
function __destruct()
{
$this->close();
}
/**
* Close zip archive and release input stream.
*/
public function close()
{
$this->length = null;
if ($this->inputStream !== null) {
fclose($this->inputStream);
$this->inputStream = null;
}
}
/**
* Whether a offset exists
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param string $entryName An offset to check for.
* @return boolean true on success or false on failure.
* The return value will be casted to boolean if non-boolean was returned.
*/
public function offsetExists($entryName)
{
return isset($this->entries[$entryName]);
}
/**
* Offset to retrieve
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param string $entryName The offset to retrieve.
* @return string|null
*/
public function offsetGet($entryName)
{
return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null;
}
/**
* Offset to set
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param string $entryName The offset to assign the value to.
* @param mixed $value The value to set.
* @throws ZipUnsupportMethod
*/
public function offsetSet($entryName, $value)
{
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
}
/**
* Offset to unset
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param string $entryName The offset to unset.
* @throws ZipUnsupportMethod
*/
public function offsetUnset($entryName)
{
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @return mixed Can return any type.
* @since 5.0.0
*/
public function current()
{
return $this->offsetGet($this->key());
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function next()
{
next($this->entries);
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @return mixed scalar on success, or null on failure.
* @since 5.0.0
*/
public function key()
{
return key($this->entries);
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
* @since 5.0.0
*/
public function valid()
{
return $this->offsetExists($this->key());
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function rewind()
{
reset($this->entries);
}
}

1400
src/PhpZip/ZipOutputFile.php Normal file

File diff suppressed because it is too large Load Diff