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

Add ZipModel for all changes.

This commit is contained in:
wapplay-home-linux
2017-11-13 15:33:37 +03:00
parent a72db0aa7d
commit 452c5920dd
73 changed files with 7081 additions and 3431 deletions

View File

@@ -1,10 +1,12 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
use PhpZip\Util\PackUtil;
/**
* Traditional PKWARE Encryption Engine.
@@ -13,7 +15,7 @@ use PhpZip\Util\CryptoUtil;
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class TraditionalPkwareEncryptionEngine implements CryptoEngine
class TraditionalPkwareEncryptionEngine implements ZipEncryptionEngine
{
/**
* Encryption header size
@@ -66,7 +68,6 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
* @var array
*/
private $keys = [];
/**
* @var ZipEntry
*/
@@ -80,7 +81,6 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
$this->initKeys($entry->getPassword());
}
/**
@@ -107,25 +107,8 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
{
$this->keys[0] = self::crc32($this->keys[0], $charAt);
$this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff);
$this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1);
$this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff));
}
/**
* Cast to int
*
* @param $i
* @return int
*/
private static function toInt($i)
{
$i = (int)($i & 0xffffffff);
if ($i > 2147483647) {
return -(-$i & 0xffffffff);
} elseif ($i < -2147483648) {
return $i & -2147483648;
}
return $i;
$this->keys[1] = PackUtil::toSignedInt32($this->keys[1] * 134775813 + 1);
$this->keys[2] = PackUtil::toSignedInt32(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff));
}
/**
@@ -147,7 +130,11 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
*/
public function decrypt($content)
{
$password = $this->entry->getPassword();
$this->initKeys($password);
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
$byte = 0;
foreach ($headerBytes as &$byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte);
@@ -198,7 +185,9 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted.
$this->initKeys($this->entry->getPassword());
$password = $this->entry->getPassword();
$this->initKeys($password);
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
$headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
@@ -233,4 +222,4 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
$this->updateKeys($byte);
return $tempVal;
}
}
}

View File

@@ -1,20 +1,22 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
/**
* WinZip Aes Encryption Engine.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEngine implements CryptoEngine
class WinZipAesEngine implements ZipEncryptionEngine
{
/**
* The block size of the Advanced Encryption Specification (AES) Algorithm
@@ -50,13 +52,16 @@ class WinZipAesEngine implements CryptoEngine
*/
public function decrypt($content)
{
$extraFieldsCollection = $this->entry->getExtraFieldsCollection();
if (!isset($extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()])) {
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
}
/**
* @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)");
}
$field = $extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()];
// Get key strength.
$keyStrengthBits = $field->getKeyStrength();
@@ -218,8 +223,8 @@ class WinZipAesEngine implements CryptoEngine
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99);
$keyStrengthBytes = 32;
$keyStrengthBits = $keyStrengthBytes * 8;
$keyStrengthBits = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($this->entry->getEncryptionMethod());
$keyStrengthBytes = $keyStrengthBits / 8;
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
@@ -244,4 +249,4 @@ class WinZipAesEngine implements CryptoEngine
substr($mac, 0, 10)
);
}
}
}

View File

@@ -1,9 +1,17 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
interface CryptoEngine
/**
* Encryption Engine
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipEncryptionEngine
{
/**
* Decryption string.
@@ -21,4 +29,4 @@ interface CryptoEngine
* @return string
*/
public function encrypt($content);
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -6,7 +7,7 @@ namespace PhpZip\Exception;
* 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.
* The exception detail message is the name of the ZIP entry.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
@@ -36,12 +37,8 @@ class Crc32Exception extends ZipException
*/
public function __construct($name, $expected, $actual)
{
parent::__construct($name
. " (expected CRC32 value 0x"
. dechex($expected)
. ", but is actually 0x"
. dechex($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;
@@ -66,5 +63,4 @@ class Crc32Exception extends ZipException
{
return $this->actualCrc;
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -8,7 +9,6 @@ namespace PhpZip\Exception;
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class InvalidArgumentException extends ZipException
class InvalidArgumentException extends RuntimeException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -9,5 +10,4 @@ namespace PhpZip\Exception;
*/
class RuntimeException extends ZipException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -9,5 +10,4 @@ namespace PhpZip\Exception;
*/
class ZipAuthenticationException extends ZipCryptoException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/
class ZipCryptoException extends ZipException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/
class ZipException extends \Exception
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/
class ZipNotFoundEntry extends ZipException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/
class ZipUnsupportMethod extends ZipException
{
}
}

View File

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

@@ -1,120 +1,35 @@
<?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.
* 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
interface ExtraField
{
/** 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.
* 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.
*
* @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.
* @return int
*/
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 ($extraField::getHeaderId() !== $headerId) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
$extraField = new DefaultExtraField($headerId);
}
return $extraField;
}
public static function getHeaderId();
/**
* Registered extra field classes.
*
* @return array|null
* Serializes a Data Block.
* @return string
*/
private static function getRegistry()
{
if (null === self::$registry) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class;
self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class;
}
return self::$registry;
}
public function serialize();
/**
* Returns a protective copy of the Data Block.
*
* @return resource
* @throws ZipException If size data block out of range.
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function getDataBlock()
{
$size = $this->getDataSize();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size data block out of range.');
}
$fp = fopen('php://memory', '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);
}
public function deserialize($data);
}

View File

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

@@ -1,200 +0,0 @@
<?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://memory', 'r+b');
$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);
$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;
}
/**
* 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);
$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,240 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\InvalidArgumentException;
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 ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator
{
/**
* 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[]
*/
protected $collection = [];
/**
* Returns the number of Extra Fields in this collection.
*
* @return int
*/
public function count()
{
return sizeof($this->collection);
}
/**
* 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->collection[$headerId])) {
return $this->collection[$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->collection[$headerId] = $extraField;
return $extraField;
}
/**
* Returns Extra Field exists
*
* @param int $headerId The requested Header ID.
* @return bool
*/
public function has($headerId)
{
return isset($this->collection[$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->collection[$headerId])) {
$ef = $this->collection[$headerId];
unset($this->collection[$headerId]);
return $ef;
}
throw new ZipException('ExtraField not found');
}
/**
* Whether a offset exists
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0
*/
public function offsetExists($offset)
{
return isset($this->collection[$offset]);
}
/**
* Offset to retrieve
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Offset to set
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @throws InvalidArgumentException
* @since 5.0.0
*/
public function offsetSet($offset, $value)
{
if ($value instanceof ExtraField) {
assert($offset == $value::getHeaderId());
$this->add($value);
} else {
throw new InvalidArgumentException('value is not instanceof ' . ExtraField::class);
}
}
/**
* Offset to unset
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
/**
* 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 current($this->collection);
}
/**
* 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->collection);
}
/**
* 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->collection);
}
/**
* 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->collection);
}
/**
* If clone extra fields.
*/
public function __clone()
{
foreach ($this->collection as $k => $v) {
$this->collection[$k] = clone $v;
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\Fields\DefaultExtraField;
use PhpZip\Extra\Fields\NtfsExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\ZipEntry;
/**
* Extra Fields Factory
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ExtraFieldsFactory
{
/**
* @var array|null
*/
protected static $registry;
private function __construct()
{
}
/**
* 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 ($extraField::getHeaderId() !== $headerId) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
$extraField = new DefaultExtraField($headerId);
}
return $extraField;
}
/**
* Registered extra field classes.
*
* @return array
*/
protected static function getRegistry()
{
if (null === self::$registry) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class;
self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class;
self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::class;
}
return self::$registry;
}
/**
* @return WinZipAesEntryExtraField
*/
public static function createWinZipAesEntryExtra()
{
return new WinZipAesEntryExtraField();
}
/**
* @return NtfsExtraField
*/
public static function createNtfsExtra()
{
return new NtfsExtraField();
}
/**
* @param ZipEntry $entry
* @return Zip64ExtraField
*/
public static function createZip64Extra(ZipEntry $entry)
{
return new Zip64ExtraField($entry);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
/**
* 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 implements ExtraField
{
/**
* @var int
*/
private static $headerId;
/**
* @var string
*/
protected $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;
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
return $this->data;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function deserialize($data)
{
$this->data = $data;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Extra\ExtraField;
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 implements 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;
/**
* 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;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function deserialize($data)
{
$unpack = unpack('vtag/vsizeAttr', substr($data, 0, 4));
if (24 === $unpack['sizeAttr']) {
$tagData = substr($data, 4, $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;
}
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
$serialize = '';
if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) {
$mtimeLong = ($this->mtime + 11644473600) * 10000000;
$atimeLong = ($this->atime + 11644473600) * 10000000;
$ctimeLong = ($this->ctime + 11644473600) * 10000000;
$serialize .= pack('Vvv', 0, 1, 8 * 3)
. PackUtil::packLongLE($mtimeLong)
. PackUtil::packLongLE($atimeLong)
. PackUtil::packLongLE($ctimeLong);
}
return $serialize;
}
/**
* @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

@@ -1,7 +1,10 @@
<?php
namespace PhpZip\Extra;
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
use PhpZip\ZipFileInterface;
/**
* WinZip AES Extra Field.
@@ -11,7 +14,7 @@ use PhpZip\Exception\ZipException;
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEntryExtraField extends ExtraField
class WinZipAesEntryExtraField implements ExtraField
{
const DATA_SIZE = 7;
const VENDOR_ID = 17729; // 'A' | ('E' << 8);
@@ -32,32 +35,38 @@ class WinZipAesEntryExtraField extends ExtraField
const KEY_STRENGTH_192BIT = 192;
const KEY_STRENGTH_256BIT = 256;
private static $keyStrengths = [
protected static $keyStrengths = [
self::KEY_STRENGTH_128BIT => 0x01,
self::KEY_STRENGTH_192BIT => 0x02,
self::KEY_STRENGTH_256BIT => 0x03
];
protected static $encryptionMethods = [
self::KEY_STRENGTH_128BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128,
self::KEY_STRENGTH_192BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192,
self::KEY_STRENGTH_256BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
];
/**
* Vendor version.
*
* @var int
*/
private $vendorVersion = self::VV_AE_1;
protected $vendorVersion = self::VV_AE_1;
/**
* Encryption strength.
*
* @var int
*/
private $encryptionStrength = self::KEY_STRENGTH_256BIT;
protected $encryptionStrength = self::KEY_STRENGTH_256BIT;
/**
* Zip compression method.
*
* @var int
*/
private $method;
protected $method;
/**
* Returns the Header ID (type) of this Extra Field.
@@ -71,21 +80,6 @@ class WinZipAesEntryExtraField extends ExtraField
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.
*
@@ -155,6 +149,32 @@ class WinZipAesEntryExtraField extends ExtraField
return $this->method & 0xffff;
}
/**
* Internal encryption method.
*
* @return int
*/
public function getEncryptionMethod()
{
return isset(self::$encryptionMethods[$this->getKeyStrength()]) ?
self::$encryptionMethods[$this->getKeyStrength()] :
self::$encryptionMethods[self::KEY_STRENGTH_256BIT];
}
/**
* @param int $encryptionMethod
* @return int
* @throws ZipException
*/
public static function getKeyStrangeFromEncryptionMethod($encryptionMethod)
{
$flipKey = array_flip(self::$encryptionMethods);
if (!isset($flipKey[$encryptionMethod])) {
throw new ZipException("Unsupport encryption method " . $encryptionMethod);
}
return $flipKey[$encryptionMethod];
}
/**
* Sets compression method.
*
@@ -169,37 +189,6 @@ class WinZipAesEntryExtraField extends ExtraField
$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.
*
@@ -218,19 +207,50 @@ class WinZipAesEntryExtraField extends ExtraField
*/
public static function encryptionStrength($keyStrength)
{
return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT];
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
* Serializes a Data Block.
* @return string
*/
public function writeTo($handle, $off)
public function serialize()
{
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method));
return pack(
'vvcv',
$this->vendorVersion,
self::VENDOR_ID,
$this->encryptionStrength,
$this->method
);
}
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
* @throws ZipException
*/
public function deserialize($data)
{
$size = strlen($data);
if (self::DATA_SIZE !== $size) {
throw new ZipException('WinZip AES Extra data invalid size: ' . $size . '. Must be ' . self::DATA_SIZE);
}
/**
* @var int $vendorVersion
* @var int $vendorId
* @var int $keyStrength
* @var int $method
*/
$unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', $data);
$this->setVendorVersion($unpack['vendorVersion']);
if (self::VENDOR_ID !== $unpack['vendorId']) {
throw new ZipException('Vendor id invalid: ' . $unpack['vendorId'] . '. Must be ' . self::VENDOR_ID);
}
$this->setKeyStrength(self::keyStrength($unpack['keyStrength'])); // checked
$this->setMethod($unpack['method']);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\RuntimeException;
use PhpZip\Extra\ExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
/**
* ZIP64 Extra Field
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class Zip64ExtraField implements ExtraField
{
/** The Header ID for a ZIP64 Extended Information Extra Field. */
const ZIP64_HEADER_ID = 0x0001;
/**
* @var ZipEntry
*/
protected $entry;
/**
* Zip64ExtraField constructor.
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry = null)
{
if (null !== $entry) {
$this->setEntry($entry);
}
}
/**
* @param ZipEntry $entry
*/
public function setEntry(ZipEntry $entry)
{
$this->entry = $entry;
}
/**
* 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 0x0001;
}
/**
* Serializes a Data Block.
* @return string
* @throws RuntimeException
*/
public function serialize()
{
if (null === $this->entry) {
throw new RuntimeException("entry is null");
}
$data = '';
// Write out Uncompressed Size.
$size = $this->entry->getSize();
if (0xffffffff <= $size) {
$data .= PackUtil::packLongLE($size);
}
// Write out Compressed Size.
$compressedSize = $this->entry->getCompressedSize();
if (0xffffffff <= $compressedSize) {
$data .= PackUtil::packLongLE($compressedSize);
}
// Write out Relative Header Offset.
$offset = $this->entry->getOffset();
if (0xffffffff <= $offset) {
$data .= PackUtil::packLongLE($offset);
}
return $data;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
* @throws RuntimeException
*/
public function deserialize($data)
{
if (null === $this->entry) {
throw new RuntimeException("entry is null");
}
$off = 0;
// Read in Uncompressed Size.
$size = $this->entry->getSize();
if (0xffffffff <= $size) {
assert(0xffffffff === $size);
$this->entry->setSize(PackUtil::unpackLongLE(substr($data, $off, 8)));
$off += 8;
}
// Read in Compressed Size.
$compressedSize = $this->entry->getCompressedSize();
if (0xffffffff <= $compressedSize) {
assert(0xffffffff === $compressedSize);
$this->entry->setCompressedSize(PackUtil::unpackLongLE(substr($data, $off, 8)));
$off += 8;
}
// Read in Relative Header Offset.
$offset = $this->entry->getOffset();
if (0xffffffff <= $offset) {
assert(0xffffffff, $offset);
$this->entry->setOffset(PackUtil::unpackLongLE(substr($data, $off, 8)));
}
}
}

View File

@@ -1,176 +0,0 @@
<?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 (24 === $unpack['sizeAttr']) {
$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 (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) {
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

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Mapper;
/**
@@ -19,7 +20,7 @@ class OffsetPositionMapper extends PositionMapper
*/
public function __construct($offset)
{
$this->offset = $offset;
$this->offset = (int)$offset;
}
/**
@@ -39,4 +40,4 @@ class OffsetPositionMapper extends PositionMapper
{
return parent::unmap($position) - $this->offset;
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Mapper;
/**
@@ -26,4 +27,4 @@ class PositionMapper
{
return $position;
}
}
}

View File

@@ -1,482 +0,0 @@
<?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.
$signatureBytes = fread($inputStream, 4);
if (strlen($signatureBytes) < 4) {
throw new ZipException("Invalid zip file.");
}
$signature = unpack('V', $signatureBytes)[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];
}
/**
* @param string $entryName
* @return ZipEntry
* @throws ZipNotFoundEntry
*/
public function getModifiedEntry($entryName){
if (!isset($this->modifiedEntries[$entryName])) {
throw new ZipNotFoundEntry('Zip modified entry ' . $entryName . ' not found');
}
return $this->modifiedEntries[$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 (null === $zipAlign) {
$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 (
(null !== $this->password || $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 (null !== $this->password) {
$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->getDosTime(),
// 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

@@ -1,11 +1,6 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Util\PackUtil;
namespace PhpZip\Model;
/**
* Read End of Central Directory
@@ -77,172 +72,26 @@ class EndOfCentralDirectory
* @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;
private $entryCount;
/**
* @var bool
*/
private $zip64 = false;
/**
* @var string|null
*/
private $newComment;
/**
* @var bool
*/
private $modified;
/**
* EndOfCentralDirectory constructor.
* @param int $entryCount
* @param null|string $comment
* @param bool $zip64
*/
public function __construct()
public function __construct($entryCount, $comment, $zip64 = false)
{
$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;
$this->entryCount = $entryCount;
$this->comment = $comment;
$this->zip64 = $zip64;
}
/**
@@ -256,9 +105,9 @@ class EndOfCentralDirectory
/**
* @return int
*/
public function getCentralDirectoryEntriesSize()
public function getEntryCount()
{
return $this->centralDirectoryEntriesSize;
return $this->entryCount;
}
/**
@@ -268,152 +117,4 @@ class EndOfCentralDirectory
{
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;
}
/**
* 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,49 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Model\ZipEntry;
/**
* Entry to write to the central directory.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class OutputOffsetEntry
{
/**
* @var int
*/
private $offset;
/**
* @var ZipEntry
*/
private $entry;
/**
* @param int $pos
* @param ZipEntry $entry
*/
public function __construct($pos, ZipEntry $entry)
{
$this->offset = $pos;
$this->entry = $entry;
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @return ZipEntry
*/
public function getEntry()
{
return $this->entry;
}
}

View File

@@ -4,15 +4,14 @@ 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\Extra\ExtraFieldsCollection;
use PhpZip\Extra\ExtraFieldsFactory;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\DateTimeConverter;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFile;
use PhpZip\Util\StringUtil;
use PhpZip\ZipFileInterface;
/**
* Abstract ZIP entry.
@@ -23,16 +22,10 @@ use PhpZip\ZipFile;
*/
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)
*/
@@ -45,14 +38,14 @@ abstract class ZipAbstractEntry implements ZipEntry
* @var int
*/
private $versionNeededToExtract = 20;
/**
* @var int
*/
private $general;
/**
* @var int Compression method
*/
private $method;
/**
* @var int
*/
private $general;
/**
* @var int Dos time
*/
@@ -78,13 +71,13 @@ abstract class ZipAbstractEntry implements ZipEntry
*/
private $offset = self::UNKNOWN;
/**
* The map of Extra Fields.
* Maps from Header ID [Integer] to Extra Field [ExtraField].
* Collections of Extra Fields.
* Keys from Header ID [int] and value Extra Field [ExtraField].
* Should be null or may be empty if no Extra Fields are used.
*
* @var ExtraFields
* @var ExtraFieldsCollection
*/
private $fields;
private $extraFieldsCollection;
/**
* @var string Comment field.
*/
@@ -95,55 +88,48 @@ abstract class ZipAbstractEntry implements ZipEntry
private $password;
/**
* Encryption method.
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
* @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
* @var int
*/
private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL;
private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL;
/**
* @var int
*/
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
/**
* @param int $mask
* @return bool
* ZipAbstractEntry constructor.
*/
private function isInit($mask)
public function __construct()
{
return 0 !== ($this->init & $mask);
$this->extraFieldsCollection = new ExtraFieldsCollection();
}
/**
* @param int $mask
* @param bool $init
* @param ZipEntry $entry
*/
private function setInit($mask, $init)
public function setEntry(ZipEntry $entry)
{
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;
$this->setName($entry->getName());
$this->setPlatform($entry->getPlatform());
$this->setVersionNeededToExtract($entry->getVersionNeededToExtract());
$this->setMethod($entry->getMethod());
$this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags());
$this->setDosTime($entry->getDosTime());
$this->setCrc($entry->getCrc());
$this->setCompressedSize($entry->getCompressedSize());
$this->setSize($entry->getSize());
$this->setExternalAttributes($entry->getExternalAttributes());
$this->setOffset($entry->getOffset());
$this->setExtra($entry->getExtra());
$this->setComment($entry->getComment());
$this->setPassword($entry->getPassword());
$this->setEncryptionMethod($entry->getEncryptionMethod());
$this->setCompressionLevel($entry->getCompressionLevel());
$this->setEncrypted($entry->isEncrypted());
}
/**
@@ -174,6 +160,23 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this;
}
/**
* 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;
}
/**
* @return int Get platform
*/
@@ -204,6 +207,28 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this;
}
/**
* @param int $mask
* @return bool
*/
protected function isInit($mask)
{
return 0 !== ($this->init & $mask);
}
/**
* @param int $mask
* @param bool $init
*/
protected function setInit($mask, $init)
{
if ($init) {
$this->init |= $mask;
} else {
$this->init &= ~$mask;
}
}
/**
* Version needed to extract.
*
@@ -321,21 +346,9 @@ abstract class ZipAbstractEntry implements ZipEntry
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
* @return int
*/
public function getGeneralPurposeBitFlags()
{
@@ -355,33 +368,19 @@ abstract class ZipAbstractEntry implements ZipEntry
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;
if ($this->method === ZipFileInterface::METHOD_DEFLATED) {
$bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1);
$bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2);
if ($bit1 && !$bit2) {
$this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION;
} elseif (!$bit1 && $bit2) {
$this->compressionLevel = ZipFileInterface::LEVEL_FAST;
} elseif ($bit1 && $bit2) {
$this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST;
} else {
$this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
}
}
return $this;
}
@@ -395,6 +394,17 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
}
/**
* Returns the indexed General Purpose Bit Flag.
*
* @param int $mask
* @return bool
*/
public function getGeneralPurposeBitFlag($mask)
{
return 0 !== ($this->general & $mask);
}
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
@@ -404,17 +414,16 @@ abstract class ZipAbstractEntry implements 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());
}
$headerId = WinZipAesEntryExtraField::getHeaderId();
if (isset($this->extraFieldsCollection[$headerId])) {
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->extraFieldsCollection[$headerId];
if (self::METHOD_WINZIP_AES === $this->getMethod()) {
$this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod());
}
unset($this->extraFieldsCollection[$headerId]);
}
$this->password = null;
return $this;
@@ -428,6 +437,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/
public function setEncrypted($encrypted)
{
$encrypted = (bool)$encrypted;
$this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
return $this;
}
@@ -451,6 +461,10 @@ abstract class ZipAbstractEntry implements ZipEntry
*/
public function setMethod($method)
{
if (self::UNKNOWN === $method) {
$this->method = $method;
return $this;
}
if (0x0000 > $method || $method > 0xffff) {
throw new ZipException('method out of range');
}
@@ -458,21 +472,15 @@ abstract class ZipAbstractEntry implements ZipEntry
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:
case ZipFileInterface::METHOD_STORED:
case ZipFileInterface::METHOD_DEFLATED:
case ZipFileInterface::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)");
}
@@ -492,24 +500,6 @@ abstract class ZipAbstractEntry implements ZipEntry
return DateTimeConverter::toUnixTimestamp($this->getDosTime());
}
/**
* 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;
}
/**
* Get Dos Time
*
@@ -535,6 +525,24 @@ abstract class ZipAbstractEntry implements ZipEntry
$this->setInit(self::BIT_DATE_TIME, true);
}
/**
* 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.
*
@@ -572,123 +580,43 @@ abstract class ZipAbstractEntry implements ZipEntry
}
/**
* Return extra field from header id.
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @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)
public function isDirectory()
{
return $this->fields === null ? false : $this->fields->has($headerId);
return StringUtil::endsWith($this->name, '/');
}
/**
* Remove extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
* @return ExtraFieldsCollection
*/
public function removeExtraField($headerId)
public function &getExtraFieldsCollection()
{
return null !== $this->fields ? $this->fields->remove($headerId) : null;
return $this->extraFieldsCollection;
}
/**
* 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)
public function getExtra()
{
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));
$extraData = '';
foreach ($this->getExtraFieldsCollection() as $extraField) {
$data = $extraField->serialize();
$extraData .= pack('vv', $extraField::getHeaderId(), strlen($data));
$extraData .= $data;
}
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));
$size = strlen($extraData);
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData);
}
// 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;
return $extraData;
}
/**
@@ -701,99 +629,31 @@ abstract class ZipAbstractEntry implements ZipEntry
*
* @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)
{
$this->extraFieldsCollection = new ExtraFieldsCollection();
if (null !== $data) {
$length = strlen($data);
if (0x0000 > $length || $length > 0xffff) {
throw new ZipException("Extra Fields too large");
$extraLength = strlen($data);
if (0x0000 > $extraLength || $extraLength > 0xffff) {
throw new ZipException("Extra Fields too large: " . $extraLength);
}
}
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;
$pos = 0;
$endPos = $extraLength;
while ($pos < $endPos) {
$unpack = unpack('vheaderId/vdataSize', substr($data, $pos, 4));
$pos += 4;
$headerId = (int)$unpack['headerId'];
$dataSize = (int)$unpack['dataSize'];
$extraField = ExtraFieldsFactory::create($headerId);
if ($extraField instanceof Zip64ExtraField) {
$extraField->setEntry($this);
}
$extraField->deserialize(substr($data, $pos, $dataSize));
$pos += $dataSize;
$this->extraFieldsCollection[$headerId] = $extraField;
}
}
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;
}
/**
@@ -803,7 +663,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/
public function getComment()
{
return null != $this->comment ? $this->comment : "";
return null !== $this->comment ? $this->comment : "";
}
/**
@@ -883,7 +743,11 @@ abstract class ZipAbstractEntry implements ZipEntry
if (null !== $encryptionMethod) {
$this->setEncryptionMethod($encryptionMethod);
}
$this->setEncrypted(!empty($this->password));
if (!empty($this->password)) {
$this->setEncrypted(true);
} else {
$this->clearEncryption();
}
return $this;
}
@@ -895,6 +759,34 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this->encryptionMethod;
}
/**
* Set encryption method
*
* @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
*
* @param int $encryptionMethod
* @return ZipEntry
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod)
{
if (null !== $encryptionMethod) {
if (
ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod
&& ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 !== $encryptionMethod
&& ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 !== $encryptionMethod
&& ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 !== $encryptionMethod
) {
throw new ZipException('Invalid encryption method');
}
$this->encryptionMethod = $encryptionMethod;
}
return $this;
}
/**
* @return int
*/
@@ -908,46 +800,23 @@ abstract class ZipAbstractEntry implements ZipEntry
* @return ZipEntry
* @throws InvalidArgumentException
*/
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION)
{
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION
) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::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()
public function __clone()
{
$this->fields = $this->fields !== null ? clone $this->fields : null;
$this->extraFieldsCollection = clone $this->extraFieldsCollection;
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* Source Entry Changes
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipChangesEntry extends ZipAbstractEntry
{
/**
* @var ZipSourceEntry
*/
protected $entry;
/**
* ZipChangesEntry constructor.
* @param ZipSourceEntry $entry
*/
public function __construct(ZipSourceEntry $entry)
{
parent::__construct();
$this->entry = $entry;
$this->setEntry($entry);
}
/**
* @return bool
*/
public function isChangedContent()
{
return !(
$this->getCompressionLevel() === $this->entry->getCompressionLevel() &&
$this->getMethod() === $this->entry->getMethod() &&
$this->isEncrypted() === $this->entry->isEncrypted() &&
$this->getEncryptionMethod() === $this->entry->getEncryptionMethod() &&
$this->getPassword() === $this->entry->getPassword()
);
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return $this->entry->getEntryContent();
}
/**
* @return ZipSourceEntry
*/
public function getSourceEntry()
{
return $this->entry;
}
}

View File

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

@@ -1,13 +1,10 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFile;
use PhpZip\ZipFileInterface;
/**
* Abstract class for new zip entry.
@@ -16,12 +13,44 @@ use PhpZip\ZipFile;
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ZipNewEntry extends ZipAbstractEntry
class ZipNewEntry extends ZipAbstractEntry
{
/**
* Default compression level for bzip2
* @var resource|string|null
*/
const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
protected $content;
/**
* @var bool
*/
private $clone = false;
/**
* ZipNewEntry constructor.
* @param string|resource|null $content
* @throws InvalidArgumentException
*/
public function __construct($content = null)
{
parent::__construct();
if ($content !== null && !is_string($content) && !is_resource($content)) {
throw new InvalidArgumentException('invalid content');
}
$this->content = $content;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
if (is_resource($this->content)) {
return stream_get_contents($this->content, -1, 0);
}
return $this->content;
}
/**
* Version needed to extract.
@@ -32,237 +61,29 @@ abstract class ZipNewEntry extends ZipAbstractEntry
{
$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)
(
ZipFileInterface::METHOD_BZIP2 === $method ? 46 :
(
$this->isZip64ExtensionsRequired() ? 45 :
(ZipFileInterface::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
* Clone extra fields
*/
public function writeEntry($outputStream)
public function __clone()
{
$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->getDosTime(),
// 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 (null !== $entryContent) {
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)");
}
$this->clone = true;
parent::__clone();
}
}
public function __destruct()
{
if (!$this->clone && null !== $this->content && is_resource($this->content)) {
fclose($this->content);
$this->content = null;
}
}
}

View File

@@ -1,55 +0,0 @@
<?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 (null !== $this->stream) {
fclose($this->stream);
$this->stream = null;
}
}
}

View File

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

@@ -1,327 +0,0 @@
<?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->setDosTime($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 (null === $this->entryContent) {
if ($this->isDirectory()) {
$this->entryContent = null;
return $this->entryContent;
}
$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->getDosTime(),
// 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 (null !== $this->entryContent && is_resource($this->entryContent)) {
fclose($this->entryContent);
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
use PhpZip\Stream\ZipInputStreamInterface;
/**
* 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 ZipSourceEntry extends ZipAbstractEntry
{
/**
* Max size cached content in memory.
*/
const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 524288; // 512 kb
/**
* @var ZipInputStreamInterface
*/
protected $inputStream;
/**
* @var string|resource Cached entry content.
*/
protected $entryContent;
/**
* @var string
*/
protected $readPassword;
/**
* @var bool
*/
private $clone = false;
/**
* ZipSourceEntry constructor.
* @param ZipInputStreamInterface $inputStream
*/
public function __construct(ZipInputStreamInterface $inputStream)
{
parent::__construct();
$this->inputStream = $inputStream;
}
/**
* @return ZipInputStreamInterface
*/
public function getInputStream()
{
return $this->inputStream;
}
/**
* Returns an string content of the given entry.
*
* @return string
* @throws ZipException
*/
public function getEntryContent()
{
if (null === $this->entryContent) {
$content = $this->inputStream->readEntryContent($this);
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;
}
/**
* Clone extra fields
*/
public function __clone()
{
$this->clone = true;
parent::__clone();
}
public function __destruct()
{
if (!$this->clone && null !== $this->entryContent && is_resource($this->entryContent)) {
fclose($this->entryContent);
}
}
}

View File

@@ -1,9 +1,11 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
use PhpZip\ZipFile;
use PhpZip\Extra\ExtraFieldsCollection;
use PhpZip\ZipFileInterface;
/**
* ZIP file entry.
@@ -39,11 +41,21 @@ interface ZipEntry
const METHOD_WINZIP_AES = 99;
/** General Purpose Bit Flag mask for encrypted data. */
const GPBF_ENCRYPTED = 1;
const GPBF_ENCRYPTED = 1; // 1 << 0
// (For Methods 8 and 9 - Deflating)
// Bit 2 Bit 1
// 0 0 Normal compression
// 0 1 Maximum compression
// 1 0 Fast compression
// 1 1 Super Fast compression
const GPBF_COMPRESSION_FLAG1 = 2; // 1 << 1
const GPBF_COMPRESSION_FLAG2 = 4; // 1 << 2
/** General Purpose Bit Flag mask for data descriptor. */
const GPBF_DATA_DESCRIPTOR = 8; // 1 << 3;
const GPBF_DATA_DESCRIPTOR = 8; // 1 << 3
/** General Purpose Bit Flag mask for strong encryption. */
const GPBF_STRONG_ENCRYPTION = 64; // 1 << 6
/** General Purpose Bit Flag mask for UTF-8. */
const GPBF_UTF8 = 2048;
const GPBF_UTF8 = 2048; // 1 << 11
/** Local File Header signature. */
const LOCAL_FILE_HEADER_SIG = 0x04034B50;
@@ -76,19 +88,11 @@ interface ZipEntry
* Compressed Size 4
* Uncompressed Size 4
*/
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26; // 1 << 11;
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
/**
* @return CentralDirectory
* Default compression level for bzip2
*/
public function getCentralDirectory();
/**
* @param CentralDirectory $centralDirectory
* @return ZipEntry
*/
public function setCentralDirectory(CentralDirectory $centralDirectory);
const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
/**
* Returns the ZIP entry name.
@@ -197,7 +201,7 @@ interface ZipEntry
/**
* Returns the General Purpose Bit Flags.
*
* @return bool
* @return int
*/
public function getGeneralPurposeBitFlags();
@@ -234,14 +238,6 @@ interface ZipEntry
*/
public function isEncrypted();
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
*
* @return ZipEntry
*/
public function clearEncryption();
/**
* Sets the encryption flag for this ZIP entry.
*
@@ -250,6 +246,14 @@ interface ZipEntry
*/
public function setEncrypted($encrypted);
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
*
* @return ZipEntry
*/
public function clearEncryption();
/**
* Returns the compression method for this entry.
*
@@ -312,37 +316,9 @@ interface ZipEntry
public function setExternalAttributes($externalAttributes);
/**
* Return extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
* @return ExtraFieldsCollection
*/
public function getExtraField($headerId);
/**
* Add extra field.
*
* @param ExtraField $field
* @return ExtraField
* @throws ZipException
*/
public function addExtraField($field);
/**
* Return exists extra field from header id.
*
* @param int $headerId
* @return bool
*/
public function hasExtraField($headerId);
/**
* Remove extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
*/
public function removeExtraField($headerId);
public function getExtraFieldsCollection();
/**
* Returns a protective copy of the serialized Extra Fields.
@@ -362,8 +338,6 @@ interface ZipEntry
*
* @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);
@@ -425,8 +399,10 @@ interface ZipEntry
/**
* Set encryption method
*
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
* @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
*
* @param int $encryptionMethod
* @return ZipEntry
@@ -443,10 +419,13 @@ interface ZipEntry
public function getEntryContent();
/**
* Write local file header, encryption header, file data and data descriptor to output stream.
*
* @param resource $outputStream
* @throws ZipException
* @param int $compressionLevel
* @return ZipEntry
*/
public function writeEntry($outputStream);
}
public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION);
/**
* @return int
*/
public function getCompressionLevel();
}

View File

@@ -0,0 +1,166 @@
<?php
namespace PhpZip\Model;
/**
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipEntryMatcher implements \Countable
{
/**
* @var ZipModel
*/
protected $zipModel;
/**
* @var array
*/
protected $matches = [];
/**
* ZipEntryMatcher constructor.
* @param ZipModel $zipModel
*/
public function __construct(ZipModel $zipModel)
{
$this->zipModel = $zipModel;
}
/**
* @param string|array $entries
* @return ZipEntryMatcher
*/
public function add($entries)
{
$entries = (array)$entries;
$entries = array_map(function ($entry) {
return $entry instanceof ZipEntry ? $entry->getName() : $entry;
}, $entries);
$this->matches = array_unique(
array_merge(
$this->matches,
array_keys(
array_intersect_key(
$this->zipModel->getEntries(),
array_flip($entries)
)
)
)
);
return $this;
}
/**
* @param string $regexp
* @return ZipEntryMatcher
*/
public function match($regexp)
{
array_walk($this->zipModel->getEntries(), function (
/** @noinspection PhpUnusedParameterInspection */
$entry,
$entryName
) use ($regexp) {
if (preg_match($regexp, $entryName)) {
$this->matches[] = $entryName;
}
});
$this->matches = array_unique($this->matches);
return $this;
}
/**
* @return ZipEntryMatcher
*/
public function all()
{
$this->matches = array_keys($this->zipModel->getEntries());
return $this;
}
/**
* Callable function for all select entries.
*
* Callable function signature:
* function(string $entryName){}
*
* @param callable $callable
*/
public function invoke(callable $callable)
{
if (!empty($this->matches)) {
array_walk($this->matches, function ($entryName) use ($callable) {
call_user_func($callable, $entryName);
});
}
}
/**
* @return array
*/
public function getMatches()
{
return $this->matches;
}
public function delete()
{
array_walk($this->matches, function ($entry) {
$this->zipModel->deleteEntry($entry);
});
$this->matches = [];
}
/**
* @param string|null $password
* @param int|null $encryptionMethod
*/
public function setPassword($password, $encryptionMethod = null)
{
array_walk($this->matches, function ($entry) use ($password, $encryptionMethod) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$this->zipModel->getEntryForChanges($entry)->setPassword($password, $encryptionMethod);
}
});
}
/**
* @param int $encryptionMethod
*/
public function setEncryptionMethod($encryptionMethod)
{
array_walk($this->matches, function ($entry) use ($encryptionMethod) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$this->zipModel->getEntryForChanges($entry)->setEncryptionMethod($encryptionMethod);
}
});
}
public function disableEncryption()
{
array_walk($this->matches, function ($entry) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$entry = $this->zipModel->getEntryForChanges($entry);
$entry->clearEncryption();
}
});
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return count($this->matches);
}
}

View File

@@ -1,10 +1,11 @@
<?php
namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\NtfsExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil;
use PhpZip\ZipFile;
use PhpZip\ZipFileInterface;
/**
* Zip info
@@ -86,7 +87,8 @@ class ZipInfo
];
private static $valuesCompressionMethod = [
ZipFile::METHOD_STORED => 'no compression',
ZipEntry::UNKNOWN => 'unknown',
ZipFileInterface::METHOD_STORED => 'no compression',
1 => 'shrink',
2 => 'reduce level 1',
3 => 'reduce level 2',
@@ -94,7 +96,7 @@ class ZipInfo
5 => 'reduce level 4',
6 => 'implode',
7 => 'reserved for Tokenizing compression algorithm',
ZipFile::METHOD_DEFLATED => 'deflate',
ZipFileInterface::METHOD_DEFLATED => 'deflate',
9 => 'deflate64',
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
11 => 'reserved by PKWARE',
@@ -114,72 +116,71 @@ class ZipInfo
/**
* @var string
*/
private $path;
private $name;
/**
* @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;
private $methodName;
/**
* @var int
*/
private $compressionMethod;
/**
* @var string
*/
private $platform;
/**
* @var int
*/
private $version;
/**
* @var string
*/
private $attributes;
/**
* @var int|null
*/
private $encryptionMethod;
/**
* @var int|null
*/
private $compressionLevel;
/**
* ZipInfo constructor.
@@ -192,16 +193,17 @@ class ZipInfo
$atime = null;
$ctime = null;
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
$field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId());
if (null !== $field && $field instanceof NtfsExtraField) {
/**
* @var NtfsExtraField $field
*/
$atime = $field->getAtime();
$ctime = $field->getCtime();
$mtime = $field->getMtime();
}
$this->path = $entry->getName();
$this->name = $entry->getName();
$this->folder = $entry->isDirectory();
$this->size = $entry->getSize();
$this->compressedSize = $entry->getCompressedSize();
@@ -209,18 +211,22 @@ class ZipInfo
$this->ctime = $ctime;
$this->atime = $atime;
$this->encrypted = $entry->isEncrypted();
$this->encryptionMethod = $entry->getEncryptionMethod();
$this->comment = $entry->getComment();
$this->crc = $entry->getCrc();
$this->method = self::getMethodName($entry);
$this->compressionMethod = self::getMethodId($entry);
$this->methodName = self::getEntryMethodName($entry);
$this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract();
$this->compressionLevel = $entry->getCompressionLevel();
$attributes = str_repeat(" ", 12);
$externalAttributes = $entry->getExternalAttributes();
$xattr = (($externalAttributes >> 16) & 0xFFFF);
switch ($entry->getPlatform()) {
case self::MADE_BY_MS_DOS:
/** @noinspection PhpMissingBreakStatementInspection */
// no break
/** @noinspection PhpMissingBreakStatementInspection */
case self::MADE_BY_WINDOWS_NTFS:
if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
($xattr & 0700) !=
@@ -237,11 +243,12 @@ class ZipInfo
if ($xattr & 0x10) {
$attributes[0] = 'd';
$attributes[3] = 'x';
} else
} else {
$attributes[0] = '-';
if ($xattr & 0x08)
}
if ($xattr & 0x08) {
$attributes[0] = 'V';
else {
} else {
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
$attributes[3] = 'x';
@@ -250,6 +257,7 @@ class ZipInfo
break;
} /* else: fall through! */
// no break
default: /* assume Unix-like */
switch ($xattr & self::UNX_IFMT) {
case self::UNX_IFDIR:
@@ -284,33 +292,59 @@ class ZipInfo
$attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
$attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
if ($xattr & self::UNX_IXUSR)
if ($xattr & self::UNX_IXUSR) {
$attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
else
$attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
if ($xattr & self::UNX_IXGRP)
$attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
else
$attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
if ($xattr & self::UNX_IXOTH)
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
else
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
} else {
$attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-';
} /* S==undefined */
if ($xattr & self::UNX_IXGRP) {
$attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x';
} /* == UNX_ENFMT */
else {
$attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-';
} /* SunOS 4.1.x */
if ($xattr & self::UNX_IXOTH) {
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x';
} /* "sticky bit" */
else {
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-';
} /* T==undefined */
}
$this->attributes = trim($attributes);
}
/**
* @param ZipEntry $entry
* @return int
*/
private static function getMethodId(ZipEntry $entry)
{
$method = $entry->getMethod();
if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
/**
* @var WinZipAesEntryExtraField $field
*/
$method = $field->getMethod();
}
}
}
return $method;
}
/**
* @param ZipEntry $entry
* @return string
*/
public static function getMethodName(ZipEntry $entry)
private static function getEntryMethodName(ZipEntry $entry)
{
$return = '';
if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
/**
* @var WinZipAesEntryExtraField $field
@@ -348,34 +382,20 @@ class ZipInfo
}
/**
* @return array
* @return string
*/
public function toArray()
public function getName()
{
return [
'path' => $this->getPath(),
'folder' => $this->isFolder(),
'size' => $this->getSize(),
'compressed_size' => $this->getCompressedSize(),
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'attributes' => $this->getAttributes(),
'encrypted' => $this->isEncrypted(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method' => $this->getMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion()
];
return $this->name;
}
/**
* @return string
* @deprecated use \PhpZip\Model\ZipInfo::getName()
*/
public function getPath()
{
return $this->path;
return $this->getName();
}
/**
@@ -426,6 +446,14 @@ class ZipInfo
return $this->atime;
}
/**
* @return string
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* @return boolean
*/
@@ -452,10 +480,19 @@ class ZipInfo
/**
* @return string
* @deprecated use \PhpZip\Model\ZipInfo::getMethodName()
*/
public function getMethod()
{
return $this->method;
return $this->getMethodName();
}
/**
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
@@ -475,35 +512,76 @@ class ZipInfo
}
/**
* @return string
* @return int|null
*/
public function getAttributes()
public function getEncryptionMethod()
{
return $this->attributes;
return $this->encryptionMethod;
}
/**
* @return int|null
*/
public function getCompressionLevel()
{
return $this->compressionLevel;
}
/**
* @return int
*/
public function getCompressionMethod()
{
return $this->compressionMethod;
}
/**
* @return array
*/
public function toArray()
{
return [
'name' => $this->getName(),
'path' => $this->getName(), // deprecated
'folder' => $this->isFolder(),
'size' => $this->getSize(),
'compressed_size' => $this->getCompressedSize(),
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'attributes' => $this->getAttributes(),
'encrypted' => $this->isEncrypted(),
'encryption_method' => $this->getEncryptionMethod(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method' => $this->getMethodName(), // deprecated
'method_name' => $this->getMethodName(),
'compression_method' => $this->getCompressionMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion()
];
}
/**
* @return string
*/
function __toString()
public 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() . '", '
. 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';
return __CLASS__ . ' {'
. 'Name="' . $this->getName() . '", '
. ($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 name="' . $this->getMethodName() . '", '
. 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';
}
}
}

View File

@@ -0,0 +1,341 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Model\Entry\ZipChangesEntry;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\ZipFileInterface;
/**
* Zip Model
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipModel implements \Countable
{
/**
* @var ZipSourceEntry[]
*/
protected $inputEntries = [];
/**
* @var ZipEntry[]
*/
protected $outEntries = [];
/**
* @var string|null
*/
protected $archiveComment;
/**
* @var string|null
*/
protected $archiveCommentChanges;
/**
* @var bool
*/
protected $archiveCommentChanged = false;
/**
* @var int|null
*/
protected $zipAlign;
/**
* @var bool
*/
private $zip64;
/**
* @param ZipSourceEntry[] $entries
* @param EndOfCentralDirectory $endOfCentralDirectory
* @return ZipModel
*/
public static function newSourceModel(array $entries, EndOfCentralDirectory $endOfCentralDirectory)
{
$model = new self;
$model->inputEntries = $entries;
$model->outEntries = $entries;
$model->archiveComment = $endOfCentralDirectory->getComment();
$model->zip64 = $endOfCentralDirectory->isZip64();
return $model;
}
/**
* @return null|string
*/
public function getArchiveComment()
{
if ($this->archiveCommentChanged) {
return $this->archiveCommentChanges;
}
return $this->archiveComment;
}
/**
* @param string $comment
* @throws InvalidArgumentException
*/
public function setArchiveComment($comment)
{
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');
}
}
if ($comment !== $this->archiveComment) {
$this->archiveCommentChanges = $comment;
$this->archiveCommentChanged = true;
} else {
$this->archiveCommentChanged = false;
}
}
/**
* Specify a password for extracting files.
*
* @param null|string $password
*/
public function setReadPassword($password)
{
foreach ($this->inputEntries as $entry) {
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
}
/**
* @param string $entryName
* @param string $password
* @throws ZipNotFoundEntry
*/
public function setReadPasswordEntry($entryName, $password)
{
if (!isset($this->inputEntries[$entryName])) {
throw new ZipNotFoundEntry('Not found entry ' . $entryName);
}
if ($this->inputEntries[$entryName]->isEncrypted()) {
$this->inputEntries[$entryName]->setPassword($password);
}
}
/**
* @return int|null
*/
public function getZipAlign()
{
return $this->zipAlign;
}
/**
* @param int|null $zipAlign
*/
public function setZipAlign($zipAlign)
{
$this->zipAlign = $zipAlign === null ? null : (int)$zipAlign;
}
/**
* @return bool
*/
public function isZipAlign()
{
return $this->zipAlign != null;
}
/**
* @param null|string $writePassword
*/
public function setWritePassword($writePassword)
{
$this->matcher()->all()->setPassword($writePassword);
}
/**
* Remove password
*/
public function removePassword()
{
$this->matcher()->all()->setPassword(null);
}
public function removePasswordEntry($entryName)
{
$this->matcher()->add($entryName)->setPassword(null);
}
/**
* @return bool
*/
public function isArchiveCommentChanged()
{
return $this->archiveCommentChanged;
}
/**
* @param string|ZipEntry $old
* @param string|ZipEntry $new
* @throws InvalidArgumentException
* @throws ZipNotFoundEntry
*/
public function renameEntry($old, $new)
{
$old = $old instanceof ZipEntry ? $old->getName() : (string)$old;
$new = $new instanceof ZipEntry ? $new->getName() : (string)$new;
if (isset($this->outEntries[$new])) {
throw new InvalidArgumentException("New entry name " . $new . ' is exists.');
}
$entry = $this->getEntryForChanges($old);
$entry->setName($new);
$this->deleteEntry($old);
$this->addEntry($entry);
}
/**
* @param string|ZipEntry $entry
* @return ZipChangesEntry|ZipEntry
*/
public function getEntryForChanges($entry)
{
$entry = $this->getEntry($entry);
if ($entry instanceof ZipSourceEntry) {
$entry = new ZipChangesEntry($entry);
$this->addEntry($entry);
}
return $entry;
}
/**
* @param string|ZipEntry $entryName
* @return ZipEntry
* @throws ZipNotFoundEntry
*/
public function getEntry($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName;
if (isset($this->outEntries[$entryName])) {
return $this->outEntries[$entryName];
}
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
/**
* @param string|ZipEntry $entry
* @return bool
*/
public function deleteEntry($entry)
{
$entry = $entry instanceof ZipEntry ? $entry->getName() : (string)$entry;
if (isset($this->outEntries[$entry])) {
unset($this->outEntries[$entry]);
return true;
}
return false;
}
/**
* @param ZipEntry $entry
*/
public function addEntry(ZipEntry $entry)
{
$this->outEntries[$entry->getName()] = $entry;
}
/**
* Get all entries with changes.
*
* @return ZipEntry[]
*/
public function &getEntries()
{
return $this->outEntries;
}
/**
* @param string|ZipEntry $entryName
* @return bool
*/
public function hasEntry($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName;
return isset($this->outEntries[$entryName]);
}
/**
* Delete all entries.
*/
public function deleteAll()
{
$this->outEntries = [];
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return sizeof($this->outEntries);
}
/**
* Undo all changes done in the archive
*/
public function unchangeAll()
{
$this->outEntries = $this->inputEntries;
$this->unchangeArchiveComment();
}
/**
* Undo change archive comment
*/
public function unchangeArchiveComment()
{
$this->archiveCommentChanges = null;
$this->archiveCommentChanged = false;
}
/**
* Revert all changes done to an entry with the given name.
*
* @param string|ZipEntry $entry Entry name or ZipEntry
* @return bool
*/
public function unchangeEntry($entry)
{
$entry = $entry instanceof ZipEntry ? $entry->getName() : (string)$entry;
if (isset($this->outEntries[$entry]) && isset($this->inputEntries[$entry])) {
$this->outEntries[$entry] = $this->inputEntries[$entry];
return true;
}
return false;
}
/**
* @param int $encryptionMethod
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256)
{
$this->matcher()->all()->setEncryptionMethod($encryptionMethod);
}
/**
* @return ZipEntryMatcher
*/
public function matcher()
{
return new ZipEntryMatcher($this);
}
}

View File

@@ -0,0 +1,298 @@
<?php
namespace PhpZip\Stream;
use Psr\Http\Message\StreamInterface;
/**
* Implement PSR Message Stream
*/
class ResponseStream implements StreamInterface
{
/**
* @var array
*/
private static $readWriteHash = [
'read' => [
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a+' => true,
],
'write' => [
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
],
];
/**
* @var resource
*/
private $stream;
/**
* @var int
*/
private $size;
/**
* @var bool
*/
private $seekable;
/**
* @var bool
*/
private $readable;
/**
* @var bool
*/
private $writable;
/**
* @var array|mixed|null
*/
private $uri;
/**
* @param resource $stream Stream resource to wrap.
* @throws \InvalidArgumentException if the stream is not a stream resource
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new \InvalidArgumentException('Stream must be a resource');
}
$this->stream = $stream;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'];
$this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
$this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
$this->uri = $this->getMetadata('uri');
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
if (!$this->stream) {
return $key ? null : [];
}
$meta = stream_get_meta_data($this->stream);
return isset($meta[$key]) ? $meta[$key] : null;
}
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString()
{
if (!$this->stream) {
return '';
}
$this->rewind();
return (string)stream_get_contents($this->stream);
}
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @throws \RuntimeException on failure.
*/
public function rewind()
{
$this->seekable && rewind($this->stream);
}
/**
* Get the size of the stream if known.
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize()
{
if ($this->size !== null) {
return $this->size;
}
if (!$this->stream) {
return null;
}
// Clear the stat cache if the stream has a URI
if ($this->uri) {
clearstatcache(true, $this->uri);
}
$stats = fstat($this->stream);
if (isset($stats['size'])) {
$this->size = $stats['size'];
return $this->size;
}
return null;
}
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
public function tell()
{
return $this->stream ? ftell($this->stream) : false;
}
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
return !$this->stream || feof($this->stream);
}
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
return $this->seekable;
}
/**
* Seek to a position in the stream.
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure.
*/
public function seek($offset, $whence = SEEK_SET)
{
$this->seekable && fseek($this->stream, $offset, $whence);
}
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
return $this->writable;
}
/**
* Write data to the stream.
*
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws \RuntimeException on failure.
*/
public function write($string)
{
$this->size = null;
return $this->writable ? fwrite($this->stream, $string) : false;
}
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
return $this->readable;
}
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws \RuntimeException if an error occurs.
*/
public function read($length)
{
return $this->readable ? fread($this->stream, $length) : "";
}
/**
* Returns the remaining contents in a string
*
* @return string
* @throws \RuntimeException if unable to read or an error occurs while
* reading.
*/
public function getContents()
{
return $this->stream ? stream_get_contents($this->stream) : '';
}
/**
* Closes the stream when the destructed
*/
public function __destruct()
{
$this->close();
}
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close()
{
if (is_resource($this->stream)) {
fclose($this->stream);
}
$this->detach();
}
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach()
{
$result = $this->stream;
$this->stream = $this->size = $this->uri = null;
$this->readable = $this->writable = $this->seekable = false;
return $result;
}
}

View File

@@ -0,0 +1,532 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFileInterface;
/**
* Read zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipInputStream implements ZipInputStreamInterface
{
/**
* @var resource
*/
protected $in;
/**
* @var PositionMapper
*/
protected $mapper;
/**
* @var int The number of bytes in the preamble of this ZIP file.
*/
protected $preamble = 0;
/**
* @var int The number of bytes in the postamble of this ZIP file.
*/
protected $postamble = 0;
/**
* @var ZipModel
*/
protected $zipModel;
/**
* ZipInputStream constructor.
* @param resource $in
* @throws RuntimeException
*/
public function __construct($in)
{
if (!is_resource($in)) {
throw new RuntimeException('$in must be resource');
}
$this->in = $in;
$this->mapper = new PositionMapper();
}
/**
* @return ZipModel
*/
public function readZip()
{
$this->checkZipFileSignature();
$endOfCentralDirectory = $this->readEndOfCentralDirectory();
$entries = $this->mountCentralDirectory($endOfCentralDirectory);
$this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory);
return $this->zipModel;
}
/**
* Check zip file signature
*
* @throws ZipException if this not .ZIP file.
*/
protected function checkZipFileSignature()
{
rewind($this->in);
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
$signatureBytes = fread($this->in, 4);
if (strlen($signatureBytes) < 4) {
throw new ZipException("Invalid zip file.");
}
$signature = unpack('V', $signatureBytes)[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);
}
}
/**
* @return EndOfCentralDirectory
* @throws ZipException
*/
protected function readEndOfCentralDirectory()
{
$comment = null;
// Search for End of central directory record.
$stats = fstat($this->in);
$size = $stats['size'];
$max = $size - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($this->in, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($this->in, 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($this->in, 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']) {
$comment = fread($this->in, $data['commentLength']);
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $size - ftell($this->in);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($this->in) === $size ||
EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1]
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $data['cdSize'];
fseek($this->in, $offset, SEEK_SET);
$offset -= $data['cdPos'];
if (0 !== $offset) {
$this->mapper = new OffsetPositionMapper($offset);
}
$entryCount = $data['cdEntries'];
return new EndOfCentralDirectory($entryCount, $comment);
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1];
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8));
// total number of disks 4 bytes
$totalDisks = unpack('V', fread($this->in, 4))[1];
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1];
if (EndOfCentralDirectory::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($this->in, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = unpack('V', fread($this->in, 4))[1];
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = unpack('V', fread($this->in, 4))[1];
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($this->in, 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($this->in, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($this->in, 8));
// zip64 extensible data sector (variable size)
fseek($this->in, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
$entryCount = $cdEntries;
$zip64 = true;
return new EndOfCentralDirectory($entryCount, $comment, $zip64);
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $size - $min;
return new EndOfCentralDirectory(0, $comment);
}
/**
* 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 EndOfCentralDirectory $endOfCentralDirectory
* @return ZipEntry[]
* @throws ZipException
*/
protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory)
{
$numEntries = $endOfCentralDirectory->getEntryCount();
$entries = [];
for (; $numEntries > 0; $numEntries--) {
$entry = $this->readEntry();
// 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!");
}
if ($this->preamble + $this->postamble >= fstat($this->in)['size']) {
assert(0 === $numEntries);
$this->checkZipFileSignature();
}
return $entries;
}
/**
* @return ZipEntry
* @throws InvalidArgumentException
*/
public function readEntry()
{
// central file header signature 4 bytes (0x02014b50)
$fileHeaderSig = unpack('V', fread($this->in, 4))[1];
if (ZipOutputStreamInterface::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($this->in, 42)
);
// $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
// See appendix D of PKWARE's ZIP File Format Specification.
$name = fread($this->in, $data['fileLength']);
$entry = new ZipSourceEntry($this);
$entry->setName($name);
$entry->setVersionNeededToExtract($data['versionNeededToExtract']);
$entry->setPlatform($data['versionMadeBy'] >> 8);
$entry->setMethod($data['rawMethod']);
$entry->setGeneralPurposeBitFlags($data['gpbf']);
$entry->setDosTime($data['rawTime']);
$entry->setCrc($data['rawCrc']);
$entry->setCompressedSize($data['rawCompressedSize']);
$entry->setSize($data['rawSize']);
$entry->setExternalAttributes($data['rawExternalAttributes']);
$entry->setOffset($data['lfhOff']); // must be unmapped!
if (0 < $data['extraLength']) {
$entry->setExtra(fread($this->in, $data['extraLength']));
}
if (0 < $data['commentLength']) {
$entry->setComment(fread($this->in, $data['commentLength']));
}
return $entry;
}
/**
* @param ZipEntry $entry
* @return string
* @throws ZipException
*/
public function readEntryContent(ZipEntry $entry)
{
if ($entry->isDirectory()) {
return null;
}
if (!($entry instanceof ZipSourceEntry)) {
throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class);
}
$isEncrypted = $entry->isEncrypted();
if ($isEncrypted && null === $entry->getPassword()) {
throw new ZipException("Can not password from entry " . $entry->getName());
}
$pos = $entry->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$startPos = $pos = $this->mapper->map($pos);
fseek($this->in, $startPos);
// local file header signature 4 bytes (0x04034b50)
if (ZipEntry::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->in, 4))[1]) {
throw new ZipException($entry->getName() . " (expected Local File Header)");
}
fseek($this->in, $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->in, 4));
$pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
$method = $entry->getMethod();
fseek($this->in, $pos);
// Get raw entry content
$compressedSize = $entry->getCompressedSize();
if ($compressedSize > 0) {
$content = fread($this->in, $compressedSize);
} else {
$content = '';
}
$skipCheckCrc = false;
if ($isEncrypted) {
if (ZipEntry::METHOD_WINZIP_AES === $method) {
// Strong Encryption Specification - WinZip AES
$winZipAesEngine = new WinZipAesEngine($entry);
$content = $winZipAesEngine->decrypt($content);
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$entry->setEncryptionMethod($field->getEncryptionMethod());
$skipCheckCrc = true;
} else {
// Traditional PKWARE Decryption
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$content = $zipCryptoEngine->decrypt($content);
$entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
}
if (!$skipCheckCrc) {
// 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->in, $pos + $compressedSize);
$localCrc = unpack('V', fread($this->in, 4))[1];
if (ZipEntry::DATA_DESCRIPTOR_SIG === $localCrc) {
$localCrc = unpack('V', fread($this->in, 4))[1];
}
} else {
fseek($this->in, $startPos + 14);
// The CRC32 in the Local File Header.
$localCrc = sprintf('%u', fread($this->in, 4)[1]);
}
if ($entry->getCrc() != $localCrc) {
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
}
switch ($method) {
case ZipFileInterface::METHOD_STORED:
break;
case ZipFileInterface::METHOD_DEFLATED:
$content = gzinflate($content);
break;
case ZipFileInterface::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 (!$skipCheckCrc) {
$localCrc = sprintf('%u', crc32($content));
if ($entry->getCrc() != $localCrc) {
if ($isEncrypted) {
throw new ZipCryptoException("Wrong password");
}
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
return $content;
}
/**
* @return resource
*/
public function getStream()
{
return $this->in;
}
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out)
{
$pos = $entry->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$pos = $this->mapper->map($pos);
$extraLength = strlen($entry->getExtra());
$nameLength = strlen($entry->getName());
$length = ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $extraLength + $nameLength;
$padding = 0;
if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) {
$padding =
(
$this->zipModel->getZipAlign() -
(ftell($out->getStream()) + $length) % $this->zipModel->getZipAlign()
) % $this->zipModel->getZipAlign();
}
fseek($this->in, $pos, SEEK_SET);
if ($padding > 0) {
stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2);
fwrite($out->getStream(), pack('v', $extraLength + $padding));
fseek($this->in, 2, SEEK_CUR);
stream_copy_to_stream($this->in, $out->getStream(), $nameLength + $extraLength);
fwrite($out->getStream(), str_repeat(chr(0), $padding));
} else {
stream_copy_to_stream($this->in, $out->getStream(), $length);
}
$this->copyEntryData($entry, $out);
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
$length = 12;
if ($entry->isZip64ExtensionsRequired()) {
$length += 8;
}
stream_copy_to_stream($this->in, $out->getStream(), $length);
}
}
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out)
{
$position = $entry->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
strlen($entry->getName()) + strlen($entry->getExtra());
$length = $entry->getCompressedSize();
fseek($this->in, $position, SEEK_SET);
stream_copy_to_stream($this->in, $out->getStream(), $length);
}
public function __destruct()
{
$this->close();
}
public function close()
{
if ($this->in != null) {
fclose($this->in);
$this->in = null;
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
/**
* Read zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipInputStreamInterface
{
/**
* @return ZipModel
*/
public function readZip();
/**
* @return ZipEntry
*/
public function readEntry();
/**
* @param ZipEntry $entry
* @return string
*/
public function readEntryContent(ZipEntry $entry);
/**
* @return resource
*/
public function getStream();
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out);
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out);
public function close();
}

View File

@@ -0,0 +1,543 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraFieldsFactory;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Model\Entry\OutputOffsetEntry;
use PhpZip\Model\Entry\ZipChangesEntry;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFileInterface;
/**
* Write
* ip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStream implements ZipOutputStreamInterface
{
/**
* @var resource
*/
protected $out;
/**
* @var ZipModel
*/
protected $zipModel;
/**
* ZipOutputStream constructor.
* @param resource $out
* @param ZipModel $zipModel
* @throws InvalidArgumentException
*/
public function __construct($out, ZipModel $zipModel)
{
if (!is_resource($out)) {
throw new InvalidArgumentException('$out must be resource');
}
$this->out = $out;
$this->zipModel = $zipModel;
}
public function writeZip()
{
$entries = $this->zipModel->getEntries();
$outPosEntries = [];
foreach ($entries as $entry) {
$outPosEntries[] = new OutputOffsetEntry(ftell($this->out), $entry);
$this->writeEntry($entry);
}
$centralDirectoryOffset = ftell($this->out);
foreach ($outPosEntries as $outputEntry) {
$this->writeCentralDirectoryHeader($outputEntry);
}
$this->writeEndOfCentralDirectoryRecord($centralDirectoryOffset);
}
/**
* @param ZipEntry $entry
* @throws ZipException
*/
public function writeEntry(ZipEntry $entry)
{
if ($entry instanceof ZipSourceEntry) {
$entry->getInputStream()->copyEntry($entry, $this);
return;
}
$entryContent = $this->entryCommitChangesAndReturnContent($entry);
$offset = ftell($this->out);
$compressedSize = $entry->getCompressedSize();
$extra = $entry->getExtra();
$nameLength = strlen($entry->getName());
$extraLength = strlen($extra);
$size = $nameLength + $extraLength;
if (0xffff < $size) {
throw new ZipException(
$entry->getName() . " (the total size of " . $size .
" bytes for the name, extra fields and comment " .
"exceeds the maximum size of " . 0xffff . " bytes)"
);
}
// zip align
$padding = 0;
if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) {
$padding =
(
$this->zipModel->getZipAlign() -
(
$offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength
) % $this->zipModel->getZipAlign()
) % $this->zipModel->getZipAlign();
}
$dd = $entry->isDataDescriptorRequired();
fwrite(
$this->out,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
ZipEntry::LOCAL_FILE_HEADER_SIG,
// 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 time 2 bytes
// last mod file date 2 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$dd ? 0 : $entry->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $entry->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $entry->getSize(),
// file name length 2 bytes
$nameLength,
// extra field length 2 bytes
$extraLength + $padding
)
);
fwrite($this->out, $entry->getName());
if ($extraLength > 0) {
fwrite($this->out, $extra);
}
if ($padding > 0) {
fwrite($this->out, str_repeat(chr(0), $padding));
}
if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) {
$entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this);
} elseif (null !== $entryContent) {
fwrite($this->out, $entryContent);
}
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
assert(ZipEntry::UNKNOWN !== $entry->getSize());
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// data descriptor signature 4 bytes (0x08074b50)
// crc-32 4 bytes
fwrite($this->out, pack('VV', ZipEntry::DATA_DESCRIPTOR_SIG, $entry->getCrc()));
// compressed size 4 or 8 bytes
// uncompressed size 4 or 8 bytes
if ($entry->isZip64ExtensionsRequired()) {
fwrite($this->out, PackUtil::packLongLE($compressedSize));
fwrite($this->out, PackUtil::packLongLE($entry->getSize()));
} else {
fwrite($this->out, pack('VV', $entry->getCompressedSize(), $entry->getSize()));
}
} elseif ($entry->getCompressedSize() != $compressedSize) {
throw new ZipException(
$entry->getName() . " (expected compressed entry size of "
. $entry->getCompressedSize() . " bytes, " .
"but is actually " . $compressedSize . " bytes)"
);
}
}
/**
* @param ZipEntry $entry
* @return null|string
* @throws ZipException
*/
protected function entryCommitChangesAndReturnContent(ZipEntry $entry)
{
if (ZipEntry::UNKNOWN === $entry->getPlatform()) {
$entry->setPlatform(ZipEntry::PLATFORM_UNIX);
}
if (ZipEntry::UNKNOWN === $entry->getTime()) {
$entry->setTime(time());
}
$method = $entry->getMethod();
$encrypted = $entry->isEncrypted();
// See appendix D of PKWARE's ZIP File Format Specification.
$utf8 = true;
if ($encrypted && null === $entry->getPassword()) {
throw new ZipException("Can not password from entry " . $entry->getName());
}
// Compose General Purpose Bit Flag.
$general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0)
| ($entry->isDataDescriptorRequired() ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0)
| ($utf8 ? ZipEntry::GPBF_UTF8 : 0);
$skipCrc = false;
$entryContent = null;
$extraFieldsCollection = $entry->getExtraFieldsCollection();
if (!($entry instanceof ZipChangesEntry && !$entry->isChangedContent())) {
$entryContent = $entry->getEntryContent();
if ($entryContent !== null) {
$entry->setSize(strlen($entryContent));
$entry->setCrc(crc32($entryContent));
if (
$encrypted &&
(
ZipEntry::METHOD_WINZIP_AES === $method ||
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 ||
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 ||
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
)
) {
$field = null;
$method = $entry->getMethod();
$keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($entry->getEncryptionMethod()); // size bits
$compressedSize = $entry->getCompressedSize();
if (ZipEntry::METHOD_WINZIP_AES === $method) {
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
$method = $field->getMethod();
if (ZipEntry::UNKNOWN !== $compressedSize) {
$compressedSize -= $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
}
$entry->setMethod($method);
}
}
if (null === $field) {
$field = ExtraFieldsFactory::createWinZipAesEntryExtra();
}
$field->setKeyStrength($keyStrength);
$field->setMethod($method);
$size = $entry->getSize();
if (20 <= $size && ZipFileInterface::METHOD_BZIP2 !== $method) {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
} else {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
$skipCrc = true;
}
$extraFieldsCollection->add($field);
if (ZipEntry::UNKNOWN !== $compressedSize) {
$compressedSize += $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
$entry->setCompressedSize($compressedSize);
}
if ($skipCrc) {
$entry->setCrc(0);
}
}
switch ($method) {
case ZipFileInterface::METHOD_STORED:
break;
case ZipFileInterface::METHOD_DEFLATED:
$entryContent = gzdeflate($entryContent, $entry->getCompressionLevel());
break;
case ZipFileInterface::METHOD_BZIP2:
$compressionLevel = $entry->getCompressionLevel() === ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ?
ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION :
$entry->getCompressionLevel();
$entryContent = bzcompress($entryContent, $compressionLevel);
if (is_int($entryContent)) {
throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
}
break;
case ZipEntry::UNKNOWN:
$entryContent = $this->determineBestCompressionMethod($entry, $entryContent);
$method = $entry->getMethod();
break;
default:
throw new ZipException($entry->getName() . " (unsupported compression method " . $method . ")");
}
if (ZipFileInterface::METHOD_DEFLATED === $method) {
$bit1 = false;
$bit2 = false;
switch ($entry->getCompressionLevel()) {
case ZipFileInterface::LEVEL_BEST_COMPRESSION:
$bit1 = true;
break;
case ZipFileInterface::LEVEL_FAST:
$bit2 = true;
break;
case ZipFileInterface::LEVEL_SUPER_FAST:
$bit1 = true;
$bit2 = true;
break;
}
$general |= ($bit1 ? ZipEntry::GPBF_COMPRESSION_FLAG1 : 0);
$general |= ($bit2 ? ZipEntry::GPBF_COMPRESSION_FLAG2 : 0);
}
if ($encrypted) {
if (
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 ||
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 ||
$entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
) {
if ($skipCrc) {
$entry->setCrc(0);
}
$entry->setMethod(ZipEntry::METHOD_WINZIP_AES);
$winZipAesEngine = new WinZipAesEngine($entry);
$entryContent = $winZipAesEngine->encrypt($entryContent);
} elseif ($entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$entryContent = $zipCryptoEngine->encrypt($entryContent);
}
}
$compressedSize = strlen($entryContent);
$entry->setCompressedSize($compressedSize);
}
}
// Commit changes.
$entry->setGeneralPurposeBitFlags($general);
if ($entry->isZip64ExtensionsRequired()) {
$extraFieldsCollection->add(ExtraFieldsFactory::createZip64Extra($entry));
} elseif ($extraFieldsCollection->has(Zip64ExtraField::getHeaderId())) {
$extraFieldsCollection->remove(Zip64ExtraField::getHeaderId());
}
return $entryContent;
}
/**
* @param ZipEntry $entry
* @param string $content
* @return string
*/
protected function determineBestCompressionMethod(ZipEntry $entry, $content)
{
if (null !== $content) {
$entryContent = gzdeflate($content, $entry->getCompressionLevel());
if (strlen($entryContent) < strlen($content)) {
$entry->setMethod(ZipFileInterface::METHOD_DEFLATED);
return $entryContent;
}
$entry->setMethod(ZipFileInterface::METHOD_STORED);
}
return $content;
}
/**
* Writes a Central File Header record.
*
* @param OutputOffsetEntry $outEntry
* @throws RuntimeException
* @internal param OutPosEntry $entry
*/
protected function writeCentralDirectoryHeader(OutputOffsetEntry $outEntry)
{
$entry = $outEntry->getEntry();
$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)) {
throw new RuntimeException("invalid entry");
}
$extra = $entry->getExtra();
$extraSize = strlen($extra);
$commentLength = strlen($entry->getComment());
fwrite(
$this->out,
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->getDosTime(),
// 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
$outEntry->getOffset()
)
);
// file name (variable size)
fwrite($this->out, $entry->getName());
if (0 < $extraSize) {
// extra field (variable size)
fwrite($this->out, $extra);
}
if (0 < $commentLength) {
// file comment (variable size)
fwrite($this->out, $entry->getComment());
}
}
protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset)
{
$centralDirectoryEntriesCount = count($this->zipModel);
$position = ftell($this->out);
$centralDirectorySize = $position - $centralDirectoryOffset;
$centralDirectoryEntriesZip64 = $centralDirectoryEntriesCount > 0xffff;
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntriesCount;
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
$zip64 // ZIP64 extensions?
= $centralDirectoryEntriesZip64
|| $centralDirectorySizeZip64
|| $centralDirectoryOffsetZip64;
if ($zip64) {
// [zip64 end of central directory record]
// relative offset of the zip64 end of central directory record
$zip64EndOfCentralDirectoryOffset = $position;
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
// size of zip64 end of central
// directory record 8 bytes
fwrite($this->out, PackUtil::packLongLE(EndOfCentralDirectory::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($this->out, pack('vvVV', 63, 46, 0, 0));
// total number of entries in the
// central directory on this disk 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount));
// total number of entries in the
// central directory 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount));
// size of the central directory 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectorySize));
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryOffset));
// zip64 extensible data sector (variable size)
// [zip64 end of central directory locator]
// signature 4 bytes (0x07064b50)
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
fwrite($this->out, pack('VV', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
// relative offset of the zip64
// end of central directory record 8 bytes
fwrite($this->out, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
// total number of disks 4 bytes
fwrite($this->out, pack('V', 1));
}
$comment = $this->zipModel->getArchiveComment();
$commentLength = strlen($comment);
fwrite(
$this->out,
pack(
'VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
EndOfCentralDirectory::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($this->out, $comment);
}
}
/**
* @return resource
*/
public function getStream()
{
return $this->out;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Model\ZipEntry;
/**
* Write zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipOutputStreamInterface
{
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
public function writeZip();
/**
* @param ZipEntry $entry
*/
public function writeEntry(ZipEntry $entry);
/**
* @return resource
*/
public function getStream();
}

View File

@@ -1,8 +1,8 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
/**
* Crypto Utils
@@ -17,7 +17,7 @@ class CryptoUtil
* @return string
* @throws RuntimeException
*/
public static final function randomBytes($length)
final public static function randomBytes($length)
{
$length = (int)$length;
if (function_exists('random_bytes')) {
@@ -30,4 +30,4 @@ class CryptoUtil
throw new RuntimeException('Extension openssl or mcrypt not loaded');
}
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
@@ -39,9 +40,9 @@ class DateTimeConverter
return mktime(
($dosTime >> 11) & 0x1f, // hour
($dosTime >> 5) & 0x3f, // minute
2 * ($dosTime & 0x1f), // second
($dosTime >> 21) & 0x0f, // month
($dosTime >> 5) & 0x3f, // minute
2 * ($dosTime & 0x1f), // second
($dosTime >> 21) & 0x0f, // month
($dosTime >> 16) & 0x1f, // day
1980 + (($dosTime >> 25) & 0x7f) // year
);
@@ -74,4 +75,4 @@ class DateTimeConverter
$date['mday'] << 16 | $date['hours'] << 11 |
$date['minutes'] << 5 | $date['seconds'] >> 1);
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
@@ -60,7 +61,7 @@ class FilesUtil
$inCurrent = 0;
$chars = str_split($globPattern);
$regexPattern = '';
foreach ($chars AS $currentChar) {
foreach ($chars as $currentChar) {
switch ($currentChar) {
case '*':
$regexPattern .= ($escaping ? "\\*" : '.*');
@@ -103,19 +104,21 @@ class FilesUtil
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= ')';
$inCurrent--;
} else if ($escaping)
} elseif ($escaping) {
$regexPattern = "\\}";
else
} else {
$regexPattern = "}";
}
$escaping = false;
break;
case ',':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= '|';
} else if ($escaping)
} elseif ($escaping) {
$regexPattern .= "\\,";
else
} else {
$regexPattern = ",";
}
break;
default:
$escaping = false;
@@ -211,12 +214,15 @@ class FilesUtil
*/
public static function humanSize($size, $unit = null)
{
if (($unit === null && $size >= 1 << 30) || $unit === "GB")
if (($unit === null && $size >= 1 << 30) || $unit === "GB") {
return number_format($size / (1 << 30), 2) . "GB";
if (($unit === null && $size >= 1 << 20) || $unit === "MB")
}
if (($unit === null && $size >= 1 << 20) || $unit === "MB") {
return number_format($size / (1 << 20), 2) . "MB";
if (($unit === null && $size >= 1 << 10) || $unit === "KB")
}
if (($unit === null && $size >= 1 << 10) || $unit === "KB") {
return number_format($size / (1 << 10), 2) . "KB";
}
return number_format($size) . " bytes";
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil;
@@ -44,7 +45,7 @@ class IgnoreFilesFilterIterator extends \FilterIterator
foreach ($this->ignoreFiles as $ignoreFile) {
// handler dir and sub dir
if ($fileInfo->isDir()
&& $ignoreFile[strlen($ignoreFile) - 1] === '/'
&& StringUtil::endsWith($ignoreFile, '/')
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
) {
return false;
@@ -57,4 +58,4 @@ class IgnoreFilesFilterIterator extends \FilterIterator
}
return true;
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil;
@@ -66,4 +67,4 @@ class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
{
return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles);
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
@@ -39,10 +40,24 @@ class PackUtil
public static function unpackLongLE($value)
{
if (version_compare(PHP_VERSION, '5.6.3') >= 0) {
return current(unpack('P', $value));
return unpack('P', $value)[1];
}
$unpack = unpack('Va/Vb', $value);
return $unpack['a'] + ($unpack['b'] << 32);
}
}
/**
* Cast to signed int 32-bit
*
* @param int $int
* @return int
*/
public static function toSignedInt32($int)
{
$int = $int & 0xffffffff;
if (PHP_INT_SIZE === 8 && ($int & 0x80000000)) {
return $int - 0x100000000;
}
return $int;
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Util;
/**
@@ -25,6 +26,6 @@ class StringUtil
public static function endsWith($haystack, $needle)
{
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
&& strpos($haystack, $needle, $temp) !== false);
&& strpos($haystack, $needle, $temp) !== false);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,630 @@
<?php
namespace PhpZip;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipEntryMatcher;
use PhpZip\Model\ZipInfo;
use Psr\Http\Message\ResponseInterface;
/**
* Create, open .ZIP files, modify, get info and extract files.
*
* 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.
* Support ZipAlign functional.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator
{
/**
* Method for Stored (uncompressed) entries.
* @see ZipEntry::setMethod()
*/
const METHOD_STORED = 0;
/**
* Method for Deflated compressed entries.
* @see ZipEntry::setMethod()
*/
const METHOD_DEFLATED = 8;
/**
* Method for BZIP2 compressed entries.
* Require php extension bz2.
* @see ZipEntry::setMethod()
*/
const METHOD_BZIP2 = 12;
/**
* Default compression level.
*/
const LEVEL_DEFAULT_COMPRESSION = -1;
/**
* Compression level for fastest compression.
*/
const LEVEL_FAST = 2;
/**
* Compression level for fastest compression.
*/
const LEVEL_BEST_SPEED = 1;
const LEVEL_SUPER_FAST = self::LEVEL_BEST_SPEED;
/**
* Compression level for best compression.
*/
const LEVEL_BEST_COMPRESSION = 9;
/**
* No specified method for set encryption method to Traditional PKWARE encryption.
*/
const ENCRYPTION_METHOD_TRADITIONAL = 0;
/**
* No specified method for set encryption method to WinZip AES encryption.
* Default value 256 bit
*/
const ENCRYPTION_METHOD_WINZIP_AES = self::ENCRYPTION_METHOD_WINZIP_AES_256;
/**
* No specified method for set encryption method to WinZip AES encryption 128 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_128 = 2;
/**
* No specified method for set encryption method to WinZip AES encryption 194 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_192 = 3;
/**
* No specified method for set encryption method to WinZip AES encryption 256 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_256 = 1;
/**
* Open zip archive from file
*
* @param string $filename
* @return ZipFileInterface
* @throws InvalidArgumentException if file doesn't exists.
* @throws ZipException if can't open file.
*/
public function openFile($filename);
/**
* Open zip archive from raw string data.
*
* @param string $data
* @return ZipFileInterface
* @throws InvalidArgumentException if data not available.
* @throws ZipException if can't open temp stream.
*/
public function openFromString($data);
/**
* Open zip archive from stream resource
*
* @param resource $handle
* @return ZipFileInterface
* @throws InvalidArgumentException Invalid stream resource
* or resource cannot seekable stream
*/
public function openFromStream($handle);
/**
* @return string[] Returns the list files.
*/
public function getListFiles();
/**
* Returns the file comment.
*
* @return string The file comment.
*/
public function getArchiveComment();
/**
* Set archive comment.
*
* @param null|string $comment
* @return ZipFileInterface
* @throws InvalidArgumentException Length comment out of range
*/
public function setArchiveComment($comment = null);
/**
* Checks that the entry in the archive is a directory.
* 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);
/**
* Returns entry comment.
*
* @param string $entryName
* @return string
* @throws ZipNotFoundEntry
*/
public function getEntryComment($entryName);
/**
* Set entry comment.
*
* @param string $entryName
* @param string|null $comment
* @return ZipFileInterface
* @throws ZipNotFoundEntry
*/
public function setEntryComment($entryName, $comment = null);
/**
* Returns the entry contents.
*
* @param string $entryName
* @return string
*/
public function getEntryContents($entryName);
/**
* Checks if there is an entry in the archive.
*
* @param string $entryName
* @return bool
*/
public function hasEntry($entryName);
/**
* Get info by entry.
*
* @param string|ZipEntry $entryName
* @return ZipInfo
* @throws ZipNotFoundEntry
*/
public function getEntryInfo($entryName);
/**
* Get info by all entries.
*
* @return ZipInfo[]
*/
public function getAllInfo();
/**
* @return ZipEntryMatcher
*/
public function matcher();
/**
* 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|string|null $entries The entries to extract. It accepts either
* a single entry name or an array of names.
* @return ZipFileInterface
* @throws ZipException
*/
public function extractTo($destination, $entries = null);
/**
* Add entry from the string.
*
* @param string $localName Zip entry name.
* @param string $contents String contents.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException If incorrect data or entry name.
* @throws ZipUnsupportMethod
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFromString($localName, $contents, $compressionMethod = null);
/**
* Add entry from the file.
*
* @param string $filename Destination file.
* @param string|null $localName Zip Entry name.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipUnsupportMethod
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFile($filename, $localName = null, $compressionMethod = null);
/**
* Add entry from the stream.
*
* @param resource $stream Stream resource.
* @param string $localName Zip Entry name.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipUnsupportMethod
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFromStream($stream, $localName, $compressionMethod = null);
/**
* Add an empty directory in the zip archive.
*
* @param string $dirName
* @return ZipFileInterface
* @throws InvalidArgumentException
*/
public function addEmptyDir($dirName);
/**
* Add directory not recursively to the zip archive.
*
* @param string $inputDir Input directory
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
*/
public function addDir($inputDir, $localPath = "/", $compressionMethod = null);
/**
* Add recursive directory to the zip archive.
*
* @param string $inputDir Input directory
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipUnsupportMethod
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null);
/**
* Add directories from directory iterator.
*
* @param \Iterator $iterator Directory iterator.
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipUnsupportMethod
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFilesFromIterator(\Iterator $iterator, $localPath = '/', $compressionMethod = null);
/**
* Add files from glob pattern.
*
* @param string $inputDir Input directory
* @param string $globPattern Glob pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $compressionMethod = null);
/**
* Add files recursively from glob pattern.
*
* @param string $inputDir Input directory
* @param string $globPattern Glob pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null);
/**
* Add files from regex pattern.
*
* @param string $inputDir Search files in this directory.
* @param string $regexPattern Regex pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @internal param bool $recursive Recursive search.
*/
public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null);
/**
* Add files recursively from regex pattern.
*
* @param string $inputDir Search files in this directory.
* @param string $regexPattern Regex pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @internal param bool $recursive Recursive search.
*/
public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null);
/**
* Add array data to archive.
* Keys is local names.
* Values is contents.
*
* @param array $mapData Associative array for added to zip.
*/
public function addAll(array $mapData);
/**
* Rename the entry.
*
* @param string $oldName Old entry name.
* @param string $newName New entry name.
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipNotFoundEntry
*/
public function rename($oldName, $newName);
/**
* Delete entry by name.
*
* @param string $entryName Zip Entry name.
* @return ZipFileInterface
* @throws ZipNotFoundEntry If entry not found.
*/
public function deleteFromName($entryName);
/**
* Delete entries by glob pattern.
*
* @param string $globPattern Glob pattern
* @return ZipFileInterface
* @throws InvalidArgumentException
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function deleteFromGlob($globPattern);
/**
* Delete entries by regex pattern.
*
* @param string $regexPattern Regex pattern
* @return ZipFileInterface
* @throws InvalidArgumentException
*/
public function deleteFromRegex($regexPattern);
/**
* Delete all entries
* @return ZipFileInterface
*/
public function deleteAll();
/**
* Set compression level for new entries.
*
* @param int $compressionLevel
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
* @see ZipFileInterface::LEVEL_SUPER_FAST
* @see ZipFileInterface::LEVEL_FAST
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
* @return ZipFileInterface
*/
public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION);
/**
* @param string $entryName
* @param int $compressionLevel
* @return ZipFileInterface
* @throws ZipException
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
* @see ZipFileInterface::LEVEL_SUPER_FAST
* @see ZipFileInterface::LEVEL_FAST
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
*/
public function setCompressionLevelEntry($entryName, $compressionLevel);
/**
* @param string $entryName
* @param int $compressionMethod
* @return ZipFileInterface
* @throws ZipException
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function setCompressionMethodEntry($entryName, $compressionMethod);
/**
* zipalign is optimization to Android application (APK) files.
*
* @param int|null $align
* @return ZipFileInterface
* @link https://developer.android.com/studio/command-line/zipalign.html
*/
public function setZipAlign($align = null);
/**
* Set password to all input encrypted entries.
*
* @param string $password Password
* @return ZipFileInterface
* @deprecated using ZipFileInterface::setReadPassword()
*/
public function withReadPassword($password);
/**
* Set password to all input encrypted entries.
*
* @param string $password Password
* @return ZipFileInterface
*/
public function setReadPassword($password);
/**
* Set password to concrete input entry.
*
* @param string $entryName
* @param string $password Password
* @return ZipFileInterface
*/
public function setReadPasswordEntry($entryName, $password);
/**
* Set password for all entries for update.
*
* @param string $password If password null then encryption clear
* @param int|null $encryptionMethod Encryption method
* @return ZipFileInterface
* @deprecated using ZipFileInterface::setPassword()
*/
public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256);
/**
* Set password for zip archive
*
* @param string $password
* @param int|null $encryptionMethod Encryption method
* @return ZipFileInterface
*/
public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256);
/**
* @param string $entryName
* @param string $password
* @param int|null $encryptionMethod
* @return mixed
*/
public function setPasswordEntry($entryName, $password, $encryptionMethod = null);
/**
* Remove password for all entries for update.
* @return ZipFileInterface
* @deprecated using ZipFileInterface::removePassword()
*/
public function withoutPassword();
/**
* Remove password for all entries for update.
* @return ZipFileInterface
*/
public function removePassword();
/**
* Remove password for concrete entry.
* @param string $entryName
* @return ZipFileInterface
*/
public function removePasswordEntry($entryName);
/**
* Undo all changes done in the archive
* @return ZipFileInterface
*/
public function unchangeAll();
/**
* Undo change archive comment
* @return ZipFileInterface
*/
public function unchangeArchiveComment();
/**
* Revert all changes done to an entry with the given name.
*
* @param string|ZipEntry $entry Entry name or ZipEntry
* @return ZipFileInterface
*/
public function unchangeEntry($entry);
/**
* Save as file.
*
* @param string $filename Output filename
* @return ZipFileInterface
* @throws InvalidArgumentException
* @throws ZipException
*/
public function saveAsFile($filename);
/**
* Save as stream.
*
* @param resource $handle Output stream resource
* @return ZipFileInterface
* @throws ZipException
*/
public function saveAsStream($handle);
/**
* Output .ZIP archive as attachment.
* Die after output.
*
* @param string $outputFilename
* @param string|null $mimeType
* @throws InvalidArgumentException
*/
public function outputAsAttachment($outputFilename, $mimeType = null);
/**
* Output .ZIP archive as PSR-Message Response.
*
* @param ResponseInterface $response
* @param string $outputFilename
* @param string|null $mimeType
* @return ResponseInterface
* @throws InvalidArgumentException
*/
public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null);
/**
* Returns the zip archive as a string.
* @return string
* @throws InvalidArgumentException
*/
public function outputAsString();
/**
* Save and reopen zip archive.
* @return ZipFileInterface
* @throws ZipException
*/
public function rewrite();
/**
* Close zip archive and release input stream.
*/
public function close();
}