mirror of
https://github.com/Ne-Lexa/php-zip.git
synced 2025-08-10 09:23:58 +02:00
Merge ZipFile and ZipOutputFile, optimization update archive.
This commit is contained in:
59
README.md
59
README.md
@@ -1,12 +1,20 @@
|
||||
`PhpZip` Version 2
|
||||
================
|
||||
`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP.
|
||||
`PhpZip` (ver 3.0.+)
|
||||
====================
|
||||
`PhpZip` - php library for manipulating zip archives.
|
||||
|
||||
The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
|
||||
|
||||
ZIP64 extensions are automatically and transparently activated when reading or writing ZIP files of more than 4 GB size.
|
||||
|
||||
The library does not require extension `php-zip` and class `ZipArchive`.
|
||||
Features:
|
||||
---------
|
||||
- Opening and unzipping zip files.
|
||||
- Create zip files.
|
||||
- Update zip files.
|
||||
- Pure php (not require extension `php-zip` and class `\ZipArchive`).
|
||||
- Output the modified archive as a string or output to the browser without saving the result to disk.
|
||||
- Support archive comment and entries comments.
|
||||
- Get info of zip entries.
|
||||
- Support zip password for PHP < 5.6.0 (`\ZipArchive` required this version), include update and remove password.
|
||||
- Support encryption method `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption`.
|
||||
- Support `ZIP64` (size > 4 GiB or files > 65535 in a .ZIP archive).
|
||||
- Support archive alignment functional [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
|
||||
|
||||
Requirements
|
||||
------------
|
||||
@@ -17,19 +25,44 @@ Requirements
|
||||
|
||||
Installation
|
||||
------------
|
||||
`composer require nelexa/zip`
|
||||
`composer require nelexa/zip:^3.0`
|
||||
|
||||
Samples
|
||||
-------
|
||||
```php
|
||||
// create archive
|
||||
$zipFile = new \PhpZip\ZipFile();
|
||||
$zipFile->addFromString("zip/entry/filename", "Is file content")
|
||||
->addFile("/path/to/file", "data/tofile")
|
||||
->addDir(__DIR__, "to/path/")
|
||||
->saveAsFile($outputFilename)
|
||||
->close();
|
||||
|
||||
// open archive, extract, add files, set password and output to browser.
|
||||
$zipFile->openFile($outputFilename)
|
||||
->extractTo($outputDirExtract)
|
||||
->deleteFromRegex('~^\.~') // delete all hidden (Unix) files
|
||||
->addFromString('dir/file.txt', 'Test file')
|
||||
->withNewPassword('password')
|
||||
->outputAsAttachment('library.jar');
|
||||
```
|
||||
Other examples can be found in the `tests/` folder
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
#### Class `\PhpZip\ZipFile` (open, extract, info)
|
||||
|
||||
|
||||
|
||||
Open zip archive from file.
|
||||
```php
|
||||
$zipFile = \PhpZip\ZipFile::openFromFile($filename);
|
||||
$zipFile = new \PhpZip\ZipFile();
|
||||
$zipFile->openFile($filename);
|
||||
```
|
||||
Open zip archive from data string.
|
||||
```php
|
||||
$data = file_get_contents($filename);
|
||||
$zipFile = \PhpZip\ZipFile::openFromString($data);
|
||||
$data = file_get_contents($urlOrFile);
|
||||
$zipFile = new \PhpZip\ZipFile();
|
||||
$zipFile->openFromString($filename);
|
||||
```
|
||||
Open zip archive from stream resource.
|
||||
```php
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"zip",
|
||||
"unzip",
|
||||
"archive",
|
||||
"extract",
|
||||
"winzip",
|
||||
@@ -23,16 +24,22 @@
|
||||
"minimum-stability": "stable",
|
||||
"require": {
|
||||
"php-64bit": "^5.4 || ^7.0",
|
||||
"ext-mbstring": "*"
|
||||
"ext-mbstring": "*",
|
||||
"nelexa/buffer": "^1.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "src/"
|
||||
"PhpZip\\": "src/PhpZip"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"": "tests/"
|
||||
"PhpZip\\": "tests/PhpZip"
|
||||
}
|
||||
},
|
||||
"suggest": {
|
||||
"ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt",
|
||||
"ext-mcrypt": "Needed to support encrypt zip entries or use ext-openssl",
|
||||
"ext-bz2": "Needed to support BZIP2 compression"
|
||||
}
|
||||
}
|
||||
|
24
src/PhpZip/Crypto/CryptoEngine.php
Normal file
24
src/PhpZip/Crypto/CryptoEngine.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
|
||||
interface CryptoEngine
|
||||
{
|
||||
/**
|
||||
* Decryption string.
|
||||
*
|
||||
* @param string $encryptionContent
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
*/
|
||||
public function decrypt($encryptionContent);
|
||||
|
||||
/**
|
||||
* Encryption string.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($content);
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\CryptoUtil;
|
||||
|
||||
@@ -12,7 +13,7 @@ use PhpZip\Util\CryptoUtil;
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class TraditionalPkwareEncryptionEngine
|
||||
class TraditionalPkwareEncryptionEngine implements CryptoEngine
|
||||
{
|
||||
/**
|
||||
* Encryption header size
|
||||
@@ -154,7 +155,7 @@ class TraditionalPkwareEncryptionEngine
|
||||
|
||||
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
// compare against the file type from extended local headers
|
||||
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
|
||||
$checkByte = ($this->entry->getTime() >> 8) & 0xff;
|
||||
} else {
|
||||
// compare against the CRC otherwise
|
||||
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
|
||||
@@ -187,11 +188,13 @@ class TraditionalPkwareEncryptionEngine
|
||||
* Encryption data
|
||||
*
|
||||
* @param string $data
|
||||
* @param int $crc
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($data, $crc)
|
||||
public function encrypt($data)
|
||||
{
|
||||
$crc = ($this->entry->isDataDescriptorRequired() ?
|
||||
($this->entry->getTime() & 0x0000ffff) << 16 :
|
||||
$this->entry->getCrc());
|
||||
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
|
||||
|
||||
// Initialize again since the generated bytes were encrypted.
|
||||
@@ -206,11 +209,12 @@ class TraditionalPkwareEncryptionEngine
|
||||
/**
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @throws ZipCryptoException
|
||||
*/
|
||||
private function encryptData($content)
|
||||
{
|
||||
if ($content === null) {
|
||||
throw new \RuntimeException();
|
||||
throw new ZipCryptoException('content is null');
|
||||
}
|
||||
$buff = '';
|
||||
foreach (unpack('C*', $content) as $val) {
|
||||
@@ -223,7 +227,7 @@ class TraditionalPkwareEncryptionEngine
|
||||
* @param int $byte
|
||||
* @return int
|
||||
*/
|
||||
protected function encryptByte($byte)
|
||||
private function encryptByte($byte)
|
||||
{
|
||||
$tempVal = $byte ^ $this->decryptByte() & 0xff;
|
||||
$this->updateKeys($byte);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\RuntimeException;
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
@@ -13,7 +14,7 @@ use PhpZip\Util\CryptoUtil;
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class WinZipAesEngine
|
||||
class WinZipAesEngine implements CryptoEngine
|
||||
{
|
||||
/**
|
||||
* The block size of the Advanced Encryption Specification (AES) Algorithm
|
||||
@@ -42,12 +43,12 @@ class WinZipAesEngine
|
||||
/**
|
||||
* Decrypt from stream resource.
|
||||
*
|
||||
* @param resource $stream Input stream resource
|
||||
* @param string $content Input stream buffer
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
* @throws ZipCryptoException
|
||||
*/
|
||||
public function decrypt($stream)
|
||||
public function decrypt($content)
|
||||
{
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
@@ -57,20 +58,20 @@ class WinZipAesEngine
|
||||
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
|
||||
}
|
||||
|
||||
$pos = ftell($stream);
|
||||
|
||||
// Get key strength.
|
||||
$keyStrengthBits = $field->getKeyStrength();
|
||||
$keyStrengthBytes = $keyStrengthBits / 8;
|
||||
|
||||
$salt = fread($stream, $keyStrengthBytes / 2);
|
||||
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8);
|
||||
$pos = $keyStrengthBytes / 2;
|
||||
$salt = substr($content, 0, $pos);
|
||||
$passwordVerifier = substr($content, $pos, self::PWD_VERIFIER_BITS / 8);
|
||||
$pos += self::PWD_VERIFIER_BITS / 8;
|
||||
|
||||
$sha1Size = 20;
|
||||
|
||||
// Init start, end and size of encrypted data.
|
||||
$endPos = $pos + $this->entry->getCompressedSize();
|
||||
$start = ftell($stream);
|
||||
$start = $pos;
|
||||
$endPos = strlen($content);
|
||||
$footerSize = $sha1Size / 2;
|
||||
$end = $endPos - $footerSize;
|
||||
$size = $end - $start;
|
||||
@@ -80,9 +81,8 @@ class WinZipAesEngine
|
||||
}
|
||||
|
||||
// Load authentication code.
|
||||
fseek($stream, $end, SEEK_SET);
|
||||
$authenticationCode = fread($stream, $footerSize);
|
||||
if (ftell($stream) !== $endPos) {
|
||||
$authenticationCode = substr($content, $end, $footerSize);
|
||||
if ($end + $footerSize !== $endPos) {
|
||||
// This should never happen unless someone is writing to the
|
||||
// end of the file concurrently!
|
||||
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
|
||||
@@ -95,27 +95,33 @@ class WinZipAesEngine
|
||||
// WinZip 99-character limit
|
||||
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
|
||||
$password = substr($password, 0, 99);
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
do {
|
||||
// Here comes the strange part about WinZip AES encryption:
|
||||
// Its unorthodox use of the Password-Based Key Derivation
|
||||
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
|
||||
// Yes, the password verifier is only a 16 bit value.
|
||||
// So we must use the MAC for password verification, too.
|
||||
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
|
||||
$keyParam = hash_pbkdf2(
|
||||
"sha1",
|
||||
$password,
|
||||
$salt,
|
||||
self::ITERATION_COUNT,
|
||||
(2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8,
|
||||
true
|
||||
);
|
||||
$key = substr($keyParam, 0, $keyStrengthBytes);
|
||||
|
||||
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
|
||||
// Verify password.
|
||||
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
|
||||
|
||||
$content = stream_get_contents($stream, $size, $start);
|
||||
$content = substr($content, $start, $size);
|
||||
$mac = hash_hmac('sha1', $content, $sha1MacParam, true);
|
||||
|
||||
if ($authenticationCode !== substr($mac, 0, 10)) {
|
||||
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)");
|
||||
throw new ZipAuthenticationException($this->entry->getName() .
|
||||
" (authenticated WinZip AES entry content has been tampered with)");
|
||||
}
|
||||
|
||||
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
|
||||
@@ -161,6 +167,7 @@ class WinZipAesEngine
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Encrypted data
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private static function encryptCtr($data, $key, $iv)
|
||||
{
|
||||
@@ -170,7 +177,7 @@ class WinZipAesEngine
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
throw new RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +188,7 @@ class WinZipAesEngine
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Raw data
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private static function decryptCtr($data, $key, $iv)
|
||||
{
|
||||
@@ -190,7 +198,7 @@ class WinZipAesEngine
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
throw new RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ namespace PhpZip\Exception;
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class IllegalArgumentException extends ZipException
|
||||
class InvalidArgumentException extends ZipException
|
||||
{
|
||||
|
||||
}
|
13
src/PhpZip/Exception/RuntimeException.php
Normal file
13
src/PhpZip/Exception/RuntimeException.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Runtime exception.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class RuntimeException extends ZipException
|
||||
{
|
||||
|
||||
}
|
@@ -80,7 +80,7 @@ abstract class ExtraField implements ExtraFieldHeader
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size data block out of range.');
|
||||
}
|
||||
$fp = fopen('php://temp', 'r+b');
|
||||
$fp = fopen('php://memory', 'r+b');
|
||||
if (0 === $size) return $fp;
|
||||
$this->writeTo($fp, 0);
|
||||
rewind($fp);
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
@@ -118,8 +117,17 @@ class ExtraFields
|
||||
}
|
||||
if (0 === $size) return '';
|
||||
|
||||
$fp = fopen('php://temp', 'r+b');
|
||||
$this->writeTo($fp, 0);
|
||||
$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);
|
||||
@@ -148,27 +156,6 @@ class ExtraFields
|
||||
return $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a list of Extra Fields of ExtraField::getExtraLength bytes to the
|
||||
* stream resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset
|
||||
*/
|
||||
private function writeTo($handle, $off)
|
||||
{
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
/**
|
||||
* @var ExtraField $ef
|
||||
*/
|
||||
foreach ($this->extra as $ef) {
|
||||
fwrite($handle, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
|
||||
$off += 4;
|
||||
fwrite($handle, $ef->writeTo($handle, $off));
|
||||
$off += $ef->getDataSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
@@ -187,7 +174,7 @@ class ExtraFields
|
||||
if (null !== $handle && 0 < $size) {
|
||||
$end = $off + $size;
|
||||
while ($off < $end) {
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
fseek($handle, $off);
|
||||
$unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
|
||||
$off += 4;
|
||||
$extraField = ExtraField::create($unpack['headerId']);
|
||||
|
466
src/PhpZip/Model/CentralDirectory.php
Normal file
466
src/PhpZip/Model/CentralDirectory.php
Normal file
@@ -0,0 +1,466 @@
|
||||
<?php
|
||||
namespace PhpZip\Model;
|
||||
|
||||
use PhpZip\Exception\InvalidArgumentException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Exception\ZipNotFoundEntry;
|
||||
use PhpZip\Model\Entry\ZipNewStringEntry;
|
||||
use PhpZip\Model\Entry\ZipReadEntry;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Read Central Directory
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class CentralDirectory
|
||||
{
|
||||
/** Central File Header signature. */
|
||||
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
|
||||
/**
|
||||
* @var EndOfCentralDirectory End of Central Directory
|
||||
*/
|
||||
private $endOfCentralDirectory;
|
||||
/**
|
||||
* @var ZipEntry[] Maps entry names to zip entries.
|
||||
*/
|
||||
private $entries = [];
|
||||
/**
|
||||
* @var ZipEntry[] New and modified entries
|
||||
*/
|
||||
private $modifiedEntries = [];
|
||||
/**
|
||||
* @var int Default compression level for the methods DEFLATED and BZIP2.
|
||||
*/
|
||||
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
|
||||
/**
|
||||
* @var int|null ZipAlign setting
|
||||
*/
|
||||
private $zipAlign;
|
||||
/**
|
||||
* @var string New password
|
||||
*/
|
||||
private $password;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $encryptionMethod;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $clearPassword;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->endOfCentralDirectory = new EndOfCentralDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the central directory from the given seekable byte channel
|
||||
* and populates the internal tables with ZipEntry instances.
|
||||
*
|
||||
* The ZipEntry's will know all data that can be obtained from the
|
||||
* central directory alone, but not the data that requires the local
|
||||
* file header or additional data to be read.
|
||||
*
|
||||
* @param resource $inputStream
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function mountCentralDirectory($inputStream)
|
||||
{
|
||||
$this->modifiedEntries = [];
|
||||
$this->checkZipFileSignature($inputStream);
|
||||
$this->endOfCentralDirectory->findCentralDirectory($inputStream);
|
||||
|
||||
$numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
|
||||
$entries = [];
|
||||
for (; $numEntries > 0; $numEntries--) {
|
||||
$entry = new ZipReadEntry($inputStream);
|
||||
$entry->setCentralDirectory($this);
|
||||
// Re-load virtual offset after ZIP64 Extended Information
|
||||
// Extra Field may have been parsed, map it to the real
|
||||
// offset and conditionally update the preamble size from it.
|
||||
$lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset());
|
||||
if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
|
||||
$this->endOfCentralDirectory->setPreamble($lfhOff);
|
||||
}
|
||||
$entries[$entry->getName()] = $entry;
|
||||
}
|
||||
|
||||
if (0 !== $numEntries % 0x10000) {
|
||||
throw new ZipException("Expected " . abs($numEntries) .
|
||||
($numEntries > 0 ? " more" : " less") .
|
||||
" entries in the Central Directory!");
|
||||
}
|
||||
$this->entries = $entries;
|
||||
|
||||
if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
|
||||
assert(0 === $numEntries);
|
||||
$this->checkZipFileSignature($inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check zip file signature
|
||||
*
|
||||
* @param resource $inputStream
|
||||
* @throws ZipException if this not .ZIP file.
|
||||
*/
|
||||
private function checkZipFileSignature($inputStream)
|
||||
{
|
||||
rewind($inputStream);
|
||||
// Constraint: A ZIP file must start with a Local File Header
|
||||
// or a (ZIP64) End Of Central Directory Record if it's empty.
|
||||
$signature = unpack('V', fread($inputStream, 4))[1];
|
||||
if (
|
||||
ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
|
||||
&& EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
|
||||
&& EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
|
||||
) {
|
||||
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set compression method for new or rewrites entries.
|
||||
* @param int $compressionLevel
|
||||
* @throws InvalidArgumentException
|
||||
* @see ZipFile::LEVEL_DEFAULT_COMPRESSION
|
||||
* @see ZipFile::LEVEL_BEST_SPEED
|
||||
* @see ZipFile::LEVEL_BEST_COMPRESSION
|
||||
*/
|
||||
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
|
||||
{
|
||||
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
|
||||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
|
||||
) {
|
||||
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
|
||||
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
|
||||
}
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZipEntry[]
|
||||
*/
|
||||
public function &getEntries()
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entryName
|
||||
* @return ZipEntry
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function getEntry($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
return $this->entries[$entryName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EndOfCentralDirectory
|
||||
*/
|
||||
public function getEndOfCentralDirectory()
|
||||
{
|
||||
return $this->endOfCentralDirectory;
|
||||
}
|
||||
|
||||
public function getArchiveComment()
|
||||
{
|
||||
return null === $this->endOfCentralDirectory->getComment() ?
|
||||
'' :
|
||||
$this->endOfCentralDirectory->getComment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry comment
|
||||
* @param string $entryName
|
||||
* @param string|null $comment
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function setEntryComment($entryName, $comment)
|
||||
{
|
||||
if (isset($this->modifiedEntries[$entryName])) {
|
||||
$this->modifiedEntries[$entryName]->setComment($comment);
|
||||
} elseif (isset($this->entries[$entryName])) {
|
||||
$entry = clone $this->entries[$entryName];
|
||||
$entry->setComment($comment);
|
||||
$this->putInModified($entryName, $entry);
|
||||
} else {
|
||||
throw new ZipNotFoundEntry("Not found entry " . $entryName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $password
|
||||
* @param int|null $encryptionMethod
|
||||
*/
|
||||
public function setNewPassword($password, $encryptionMethod = null)
|
||||
{
|
||||
$this->password = $password;
|
||||
$this->encryptionMethod = $encryptionMethod;
|
||||
$this->clearPassword = $password === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getZipAlign()
|
||||
{
|
||||
return $this->zipAlign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $zipAlign
|
||||
*/
|
||||
public function setZipAlign($zipAlign = null)
|
||||
{
|
||||
if ($zipAlign === null) {
|
||||
$this->zipAlign = null;
|
||||
return;
|
||||
}
|
||||
$this->zipAlign = (int)$zipAlign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put modification or new entries.
|
||||
*
|
||||
* @param $entryName
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function putInModified($entryName, ZipEntry $entry)
|
||||
{
|
||||
$this->modifiedEntries[$entryName] = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entryName
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function deleteEntry($entryName)
|
||||
{
|
||||
if (isset($this->entries[$entryName])) {
|
||||
$this->modifiedEntries[$entryName] = null;
|
||||
} elseif (isset($this->modifiedEntries[$entryName])) {
|
||||
unset($this->modifiedEntries[$entryName]);
|
||||
} else {
|
||||
throw new ZipNotFoundEntry("Not found entry " . $entryName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $regexPattern
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteEntriesFromRegex($regexPattern)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($this->modifiedEntries as $entryName => &$entry) {
|
||||
if (preg_match($regexPattern, $entryName)) {
|
||||
unset($entry);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
foreach ($this->entries as $entryName => $entry) {
|
||||
if (preg_match($regexPattern, $entryName)) {
|
||||
$this->modifiedEntries[$entryName] = null;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function rename($oldName, $newName)
|
||||
{
|
||||
$oldName = (string)$oldName;
|
||||
$newName = (string)$newName;
|
||||
|
||||
if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
|
||||
throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
|
||||
}
|
||||
|
||||
if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
|
||||
$newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
|
||||
$this->modifiedEntries[$oldName] :
|
||||
$this->entries[$oldName]);
|
||||
$newEntry->setName($newName);
|
||||
|
||||
$this->modifiedEntries[$oldName] = null;
|
||||
$this->modifiedEntries[$newName] = $newEntry;
|
||||
return;
|
||||
}
|
||||
throw new ZipNotFoundEntry("Not found entry " . $oldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all entries.
|
||||
*/
|
||||
public function deleteAll()
|
||||
{
|
||||
$this->modifiedEntries = [];
|
||||
foreach ($this->entries as $entry) {
|
||||
$this->modifiedEntries[$entry->getName()] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $outputStream
|
||||
*/
|
||||
public function writeArchive($outputStream)
|
||||
{
|
||||
/**
|
||||
* @var ZipEntry[] $memoryEntriesResult
|
||||
*/
|
||||
$memoryEntriesResult = [];
|
||||
foreach ($this->entries as $entryName => $entry) {
|
||||
if (isset($this->modifiedEntries[$entryName])) continue;
|
||||
|
||||
if (
|
||||
($this->password !== null || $this->clearPassword) &&
|
||||
$entry->isEncrypted() &&
|
||||
$entry->getPassword() !== null &&
|
||||
(
|
||||
$entry->getPassword() !== $this->password ||
|
||||
$entry->getEncryptionMethod() !== $this->encryptionMethod
|
||||
)
|
||||
) {
|
||||
$prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
|
||||
$prototypeEntry->setName($entry->getName());
|
||||
$prototypeEntry->setMethod($entry->getMethod());
|
||||
$prototypeEntry->setTime($entry->getTime());
|
||||
$prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
|
||||
$prototypeEntry->setExtra($entry->getExtra());
|
||||
$prototypeEntry->setPassword($this->password, $this->encryptionMethod);
|
||||
if($this->clearPassword){
|
||||
$prototypeEntry->clearEncryption();
|
||||
}
|
||||
} else {
|
||||
$prototypeEntry = clone $entry;
|
||||
}
|
||||
$memoryEntriesResult[$entryName] = $prototypeEntry;
|
||||
}
|
||||
|
||||
foreach ($this->modifiedEntries as $entryName => $outputEntry) {
|
||||
if (null === $outputEntry) { // remove marked entry
|
||||
unset($memoryEntriesResult[$entryName]);
|
||||
} else {
|
||||
if ($this->password !== null) {
|
||||
$outputEntry->setPassword($this->password, $this->encryptionMethod);
|
||||
}
|
||||
$memoryEntriesResult[$entryName] = $outputEntry;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($memoryEntriesResult as $key => $outputEntry) {
|
||||
$outputEntry->setCentralDirectory($this);
|
||||
$outputEntry->writeEntry($outputStream);
|
||||
}
|
||||
$centralDirectoryOffset = ftell($outputStream);
|
||||
foreach ($memoryEntriesResult as $key => $outputEntry) {
|
||||
if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
|
||||
unset($memoryEntriesResult[$key]);
|
||||
}
|
||||
}
|
||||
$centralDirectoryEntries = sizeof($memoryEntriesResult);
|
||||
$this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
|
||||
$outputStream,
|
||||
$centralDirectoryEntries,
|
||||
$centralDirectoryOffset
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a Central File Header record.
|
||||
*
|
||||
* @param resource $outputStream
|
||||
* @param ZipEntry $entry
|
||||
* @return bool false if and only if the record has been skipped,
|
||||
* i.e. not written for some other reason than an I/O error.
|
||||
*/
|
||||
private function writeCentralFileHeader($outputStream, ZipEntry $entry)
|
||||
{
|
||||
$compressedSize = $entry->getCompressedSize();
|
||||
$size = $entry->getSize();
|
||||
// This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
|
||||
// UNKNOWN!
|
||||
if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
|
||||
return false;
|
||||
}
|
||||
$extra = $entry->getExtra();
|
||||
$extraSize = strlen($extra);
|
||||
|
||||
$commentLength = strlen($entry->getComment());
|
||||
fwrite(
|
||||
$outputStream,
|
||||
pack(
|
||||
'VvvvvVVVVvvvvvVV',
|
||||
// central file header signature 4 bytes (0x02014b50)
|
||||
self::CENTRAL_FILE_HEADER_SIG,
|
||||
// version made by 2 bytes
|
||||
($entry->getPlatform() << 8) | 63,
|
||||
// version needed to extract 2 bytes
|
||||
$entry->getVersionNeededToExtract(),
|
||||
// general purpose bit flag 2 bytes
|
||||
$entry->getGeneralPurposeBitFlags(),
|
||||
// compression method 2 bytes
|
||||
$entry->getMethod(),
|
||||
// last mod file datetime 4 bytes
|
||||
$entry->getTime(),
|
||||
// crc-32 4 bytes
|
||||
$entry->getCrc(),
|
||||
// compressed size 4 bytes
|
||||
$entry->getCompressedSize(),
|
||||
// uncompressed size 4 bytes
|
||||
$entry->getSize(),
|
||||
// file name length 2 bytes
|
||||
strlen($entry->getName()),
|
||||
// extra field length 2 bytes
|
||||
$extraSize,
|
||||
// file comment length 2 bytes
|
||||
$commentLength,
|
||||
// disk number start 2 bytes
|
||||
0,
|
||||
// internal file attributes 2 bytes
|
||||
0,
|
||||
// external file attributes 4 bytes
|
||||
$entry->getExternalAttributes(),
|
||||
// relative offset of local header 4 bytes
|
||||
$entry->getOffset()
|
||||
)
|
||||
);
|
||||
// file name (variable size)
|
||||
fwrite($outputStream, $entry->getName());
|
||||
if (0 < $extraSize) {
|
||||
// extra field (variable size)
|
||||
fwrite($outputStream, $extra);
|
||||
}
|
||||
if (0 < $commentLength) {
|
||||
// file comment (variable size)
|
||||
fwrite($outputStream, $entry->getComment());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function release()
|
||||
{
|
||||
unset($this->entries);
|
||||
unset($this->modifiedEntries);
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
{
|
||||
$this->release();
|
||||
}
|
||||
|
||||
}
|
427
src/PhpZip/Model/EndOfCentralDirectory.php
Normal file
427
src/PhpZip/Model/EndOfCentralDirectory.php
Normal file
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
namespace PhpZip\Model;
|
||||
|
||||
use PhpZip\Exception\InvalidArgumentException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Mapper\OffsetPositionMapper;
|
||||
use PhpZip\Mapper\PositionMapper;
|
||||
use PhpZip\Util\PackUtil;
|
||||
|
||||
/**
|
||||
* Read End of Central Directory
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class EndOfCentralDirectory
|
||||
{
|
||||
/** Zip64 End Of Central Directory Record. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
|
||||
/** Zip64 End Of Central Directory Locator. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
|
||||
/** End Of Central Directory Record signature. */
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
|
||||
/**
|
||||
* The minimum length of the End Of Central Directory Record.
|
||||
*
|
||||
* end of central dir signature 4
|
||||
* number of this disk 2
|
||||
* number of the disk with the
|
||||
* start of the central directory 2
|
||||
* total number of entries in the
|
||||
* central directory on this disk 2
|
||||
* total number of entries in
|
||||
* the central directory 2
|
||||
* size of the central directory 4
|
||||
* offset of start of central *
|
||||
* directory with respect to *
|
||||
* the starting disk number 4
|
||||
* zipfile comment length 2
|
||||
*/
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
|
||||
/**
|
||||
* The length of the Zip64 End Of Central Directory Locator.
|
||||
* zip64 end of central dir locator
|
||||
* signature 4
|
||||
* number of the disk with the
|
||||
* start of the zip64 end of
|
||||
* central directory 4
|
||||
* relative offset of the zip64
|
||||
* end of central directory record 8
|
||||
* total number of disks 4
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
|
||||
/**
|
||||
* The minimum length of the Zip64 End Of Central Directory Record.
|
||||
*
|
||||
* zip64 end of central dir
|
||||
* signature 4
|
||||
* size of zip64 end of central
|
||||
* directory record 8
|
||||
* version made by 2
|
||||
* version needed to extract 2
|
||||
* number of this disk 4
|
||||
* number of the disk with the
|
||||
* start of the central directory 4
|
||||
* total number of entries in the
|
||||
* central directory on this disk 8
|
||||
* total number of entries in
|
||||
* the central directory 8
|
||||
* size of the central directory 8
|
||||
* offset of start of central
|
||||
* directory with respect to
|
||||
* the starting disk number 8
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
|
||||
/**
|
||||
* @var string|null The archive comment.
|
||||
*/
|
||||
private $comment;
|
||||
/**
|
||||
* @var int The number of bytes in the preamble of this ZIP file.
|
||||
*/
|
||||
private $preamble;
|
||||
/**
|
||||
* @var int The number of bytes in the postamble of this ZIP file.
|
||||
*/
|
||||
private $postamble;
|
||||
/**
|
||||
* @var PositionMapper Maps offsets specified in the ZIP file to real offsets in the file.
|
||||
*/
|
||||
private $mapper;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $centralDirectoryEntriesSize;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $zip64 = false;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $newComment;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $modified;
|
||||
|
||||
/**
|
||||
* EndOfCentralDirectory constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->mapper = new PositionMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the file pointer at the first Central File Header.
|
||||
* Performs some means to check that this is really a ZIP file.
|
||||
*
|
||||
* @param resource $inputStream
|
||||
* @throws ZipException If the file is not compatible to the ZIP File
|
||||
* Format Specification.
|
||||
*/
|
||||
public function findCentralDirectory($inputStream)
|
||||
{
|
||||
// Search for End of central directory record.
|
||||
$stats = fstat($inputStream);
|
||||
$size = $stats['size'];
|
||||
$max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
|
||||
$min = $max >= 0xffff ? $max - 0xffff : 0;
|
||||
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
|
||||
fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET);
|
||||
// end of central dir signature 4 bytes (0x06054b50)
|
||||
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1])
|
||||
continue;
|
||||
|
||||
// number of this disk - 2 bytes
|
||||
// number of the disk with the start of the
|
||||
// central directory - 2 bytes
|
||||
// total number of entries in the central
|
||||
// directory on this disk - 2 bytes
|
||||
// total number of entries in the central
|
||||
// directory - 2 bytes
|
||||
// size of the central directory - 4 bytes
|
||||
// offset of start of central directory with
|
||||
// respect to the starting disk number - 4 bytes
|
||||
// ZIP file comment length - 2 bytes
|
||||
$data = unpack(
|
||||
'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
|
||||
fread($inputStream, 18)
|
||||
);
|
||||
|
||||
if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
|
||||
throw new ZipException(
|
||||
"ZIP file spanning/splitting is not supported!"
|
||||
);
|
||||
}
|
||||
// .ZIP file comment (variable size)
|
||||
if (0 < $data['commentLength']) {
|
||||
$this->comment = fread($inputStream, $data['commentLength']);
|
||||
}
|
||||
$this->preamble = $endOfCentralDirRecordPos;
|
||||
$this->postamble = $size - ftell($inputStream);
|
||||
|
||||
// Check for ZIP64 End Of Central Directory Locator.
|
||||
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
|
||||
|
||||
fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET);
|
||||
// zip64 end of central dir locator
|
||||
// signature 4 bytes (0x07064b50)
|
||||
if (
|
||||
0 > $endOfCentralDirLocatorPos ||
|
||||
ftell($inputStream) === $size ||
|
||||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1]
|
||||
) {
|
||||
// Seek and check first CFH, probably requiring an offset mapper.
|
||||
$offset = $endOfCentralDirRecordPos - $data['cdSize'];
|
||||
fseek($inputStream, $offset, SEEK_SET);
|
||||
$offset -= $data['cdPos'];
|
||||
if (0 !== $offset) {
|
||||
$this->mapper = new OffsetPositionMapper($offset);
|
||||
}
|
||||
$this->centralDirectoryEntriesSize = $data['cdEntries'];
|
||||
return;
|
||||
}
|
||||
|
||||
// number of the disk with the
|
||||
// start of the zip64 end of
|
||||
// central directory 4 bytes
|
||||
$zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1];
|
||||
// relative offset of the zip64
|
||||
// end of central directory record 8 bytes
|
||||
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8));
|
||||
// total number of disks 4 bytes
|
||||
$totalDisks = unpack('V', fread($inputStream, 4))[1];
|
||||
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
|
||||
throw new ZipException("ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
|
||||
// zip64 end of central dir
|
||||
// signature 4 bytes (0x06064b50)
|
||||
$zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1];
|
||||
if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
|
||||
throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
|
||||
}
|
||||
// size of zip64 end of central
|
||||
// directory record 8 bytes
|
||||
// version made by 2 bytes
|
||||
// version needed to extract 2 bytes
|
||||
fseek($inputStream, 12, SEEK_CUR);
|
||||
// number of this disk 4 bytes
|
||||
$diskNo = unpack('V', fread($inputStream, 4))[1];
|
||||
// number of the disk with the
|
||||
// start of the central directory 4 bytes
|
||||
$cdDiskNo = unpack('V', fread($inputStream, 4))[1];
|
||||
// total number of entries in the
|
||||
// central directory on this disk 8 bytes
|
||||
$cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8));
|
||||
// total number of entries in the
|
||||
// central directory 8 bytes
|
||||
$cdEntries = PackUtil::unpackLongLE(fread($inputStream, 8));
|
||||
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
|
||||
throw new ZipException("ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
|
||||
throw new ZipException("Total Number Of Entries In The Central Directory out of range!");
|
||||
}
|
||||
// size of the central directory 8 bytes
|
||||
fseek($inputStream, 8, SEEK_CUR);
|
||||
// offset of start of central
|
||||
// directory with respect to
|
||||
// the starting disk number 8 bytes
|
||||
$cdPos = PackUtil::unpackLongLE(fread($inputStream, 8));
|
||||
// zip64 extensible data sector (variable size)
|
||||
fseek($inputStream, $cdPos, SEEK_SET);
|
||||
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
|
||||
$this->centralDirectoryEntriesSize = $cdEntries;
|
||||
$this->zip64 = true;
|
||||
return;
|
||||
}
|
||||
// Start recovering file entries from min.
|
||||
$this->preamble = $min;
|
||||
$this->postamble = $size - $min;
|
||||
$this->centralDirectoryEntriesSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCentralDirectoryEntriesSize()
|
||||
{
|
||||
return $this->centralDirectoryEntriesSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isZip64()
|
||||
{
|
||||
return $this->zip64;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPreamble()
|
||||
{
|
||||
return $this->preamble;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPostamble()
|
||||
{
|
||||
return $this->postamble;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PositionMapper
|
||||
*/
|
||||
public function getMapper()
|
||||
{
|
||||
return $this->mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $preamble
|
||||
*/
|
||||
public function setPreamble($preamble)
|
||||
{
|
||||
$this->preamble = $preamble;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set archive comment
|
||||
* @param string|null $comment
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setComment($comment = null)
|
||||
{
|
||||
if (null !== $comment && strlen($comment) !== 0) {
|
||||
$comment = (string)$comment;
|
||||
$length = strlen($comment);
|
||||
if (0x0000 > $length || $length > 0xffff) {
|
||||
throw new InvalidArgumentException('Length comment out of range');
|
||||
}
|
||||
}
|
||||
$this->modified = $comment !== $this->comment;
|
||||
$this->newComment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isModified()
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write end of central directory.
|
||||
*
|
||||
* @param resource $outputStream Output stream
|
||||
* @param int $centralDirectoryEntries Size entries
|
||||
* @param int $centralDirectoryOffset Offset central directory
|
||||
*/
|
||||
public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset)
|
||||
{
|
||||
$position = ftell($outputStream);
|
||||
$centralDirectorySize = $position - $centralDirectoryOffset;
|
||||
$centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff;
|
||||
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
|
||||
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
|
||||
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries;
|
||||
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
|
||||
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
|
||||
$zip64 // ZIP64 extensions?
|
||||
= $centralDirectoryEntriesZip64
|
||||
|| $centralDirectorySizeZip64
|
||||
|| $centralDirectoryOffsetZip64;
|
||||
if ($zip64) {
|
||||
// relative offset of the zip64 end of central directory record
|
||||
$zip64EndOfCentralDirectoryOffset = $position;
|
||||
// zip64 end of central dir
|
||||
// signature 4 bytes (0x06064b50)
|
||||
fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
|
||||
// size of zip64 end of central
|
||||
// directory record 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
|
||||
// version made by 2 bytes
|
||||
// version needed to extract 2 bytes
|
||||
// due to potential use of BZIP2 compression
|
||||
// number of this disk 4 bytes
|
||||
// number of the disk with the
|
||||
// start of the central directory 4 bytes
|
||||
fwrite($outputStream, pack('vvVV', 63, 46, 0, 0));
|
||||
// total number of entries in the
|
||||
// central directory on this disk 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
|
||||
// total number of entries in the
|
||||
// central directory 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
|
||||
// size of the central directory 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize));
|
||||
// offset of start of central
|
||||
// directory with respect to
|
||||
// the starting disk number 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset));
|
||||
// zip64 extensible data sector (variable size)
|
||||
//
|
||||
// zip64 end of central dir locator
|
||||
// signature 4 bytes (0x07064b50)
|
||||
// number of the disk with the
|
||||
// start of the zip64 end of
|
||||
// central directory 4 bytes
|
||||
fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
|
||||
// relative offset of the zip64
|
||||
// end of central directory record 8 bytes
|
||||
fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
|
||||
// total number of disks 4 bytes
|
||||
fwrite($outputStream, pack('V', 1));
|
||||
}
|
||||
$comment = $this->modified ? $this->newComment : $this->comment;
|
||||
$commentLength = strlen($comment);
|
||||
fwrite(
|
||||
$outputStream,
|
||||
pack('VvvvvVVv',
|
||||
// end of central dir signature 4 bytes (0x06054b50)
|
||||
self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
|
||||
// number of this disk 2 bytes
|
||||
0,
|
||||
// number of the disk with the
|
||||
// start of the central directory 2 bytes
|
||||
0,
|
||||
// total number of entries in the
|
||||
// central directory on this disk 2 bytes
|
||||
$centralDirectoryEntries16,
|
||||
// total number of entries in
|
||||
// the central directory 2 bytes
|
||||
$centralDirectoryEntries16,
|
||||
// size of the central directory 4 bytes
|
||||
$centralDirectorySize32,
|
||||
// offset of start of central
|
||||
// directory with respect to
|
||||
// the starting disk number 4 bytes
|
||||
$centralDirectoryOffset32,
|
||||
// .ZIP file comment length 2 bytes
|
||||
$commentLength
|
||||
)
|
||||
);
|
||||
if ($commentLength > 0) {
|
||||
// .ZIP file comment (variable size)
|
||||
fwrite($outputStream, $comment);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
926
src/PhpZip/Model/Entry/ZipAbstractEntry.php
Normal file
926
src/PhpZip/Model/Entry/ZipAbstractEntry.php
Normal file
@@ -0,0 +1,926 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Exception\InvalidArgumentException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Extra\DefaultExtraField;
|
||||
use PhpZip\Extra\ExtraField;
|
||||
use PhpZip\Extra\ExtraFields;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Model\CentralDirectory;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\DateTimeConverter;
|
||||
use PhpZip\Util\PackUtil;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Abstract ZIP entry.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class ZipAbstractEntry implements ZipEntry
|
||||
{
|
||||
/**
|
||||
* @var CentralDirectory
|
||||
*/
|
||||
private $centralDirectory;
|
||||
|
||||
/**
|
||||
* @var int Bit flags for init state.
|
||||
*/
|
||||
private $init;
|
||||
|
||||
/**
|
||||
* @var string Entry name (filename in archive)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var int Made by platform
|
||||
*/
|
||||
private $platform;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $versionNeededToExtract = 20;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $general;
|
||||
/**
|
||||
* @var int Compression method
|
||||
*/
|
||||
private $method;
|
||||
/**
|
||||
* @var int Dos time
|
||||
*/
|
||||
private $dosTime;
|
||||
/**
|
||||
* @var int Crc32
|
||||
*/
|
||||
private $crc;
|
||||
/**
|
||||
* @var int Compressed size
|
||||
*/
|
||||
private $compressedSize = self::UNKNOWN;
|
||||
/**
|
||||
* @var int Uncompressed size
|
||||
*/
|
||||
private $size = self::UNKNOWN;
|
||||
/**
|
||||
* @var int External attributes
|
||||
*/
|
||||
private $externalAttributes;
|
||||
/**
|
||||
* @var int Relative Offset Of Local File Header.
|
||||
*/
|
||||
private $offset = self::UNKNOWN;
|
||||
/**
|
||||
* The map of Extra Fields.
|
||||
* Maps from Header ID [Integer] to Extra Field [ExtraField].
|
||||
* Should be null or may be empty if no Extra Fields are used.
|
||||
*
|
||||
* @var ExtraFields
|
||||
*/
|
||||
private $fields;
|
||||
/**
|
||||
* @var string Comment field.
|
||||
*/
|
||||
private $comment;
|
||||
/**
|
||||
* @var string Entry password for read or write encryption data.
|
||||
*/
|
||||
private $password;
|
||||
/**
|
||||
* Encryption method.
|
||||
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
|
||||
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
|
||||
* @var int
|
||||
*/
|
||||
private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
|
||||
|
||||
/**
|
||||
* @param int $mask
|
||||
* @return bool
|
||||
*/
|
||||
private function isInit($mask)
|
||||
{
|
||||
return 0 !== ($this->init & $mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $mask
|
||||
* @param bool $init
|
||||
*/
|
||||
private function setInit($mask, $init)
|
||||
{
|
||||
if ($init) {
|
||||
$this->init |= $mask;
|
||||
} else {
|
||||
$this->init &= ~$mask;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CentralDirectory
|
||||
*/
|
||||
public function getCentralDirectory()
|
||||
{
|
||||
return $this->centralDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CentralDirectory $centralDirectory
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setCentralDirectory(CentralDirectory $centralDirectory)
|
||||
{
|
||||
$this->centralDirectory = $centralDirectory;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ZIP entry name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry name.
|
||||
*
|
||||
* @param string $name New entry name
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$length = strlen($name);
|
||||
if (0x0000 > $length || $length > 0xffff) {
|
||||
throw new ZipException('Illegal zip entry name parameter');
|
||||
}
|
||||
$encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true);
|
||||
$this->setGeneralPurposeBitFlag(self::GPBF_UTF8, $encoding === 'UTF-8');
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Get platform
|
||||
*/
|
||||
public function getPlatform()
|
||||
{
|
||||
return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set platform
|
||||
*
|
||||
* @param int $platform
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setPlatform($platform)
|
||||
{
|
||||
$known = self::UNKNOWN !== $platform;
|
||||
if ($known) {
|
||||
if (0x00 > $platform || $platform > 0xff) {
|
||||
throw new ZipException("Platform out of range");
|
||||
}
|
||||
$this->platform = $platform;
|
||||
} else {
|
||||
$this->platform = 0;
|
||||
}
|
||||
$this->setInit(self::BIT_PLATFORM, $known);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version needed to extract.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getVersionNeededToExtract()
|
||||
{
|
||||
return $this->versionNeededToExtract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set version needed to extract.
|
||||
*
|
||||
* @param int $version
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setVersionNeededToExtract($version)
|
||||
{
|
||||
$this->versionNeededToExtract = $version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isZip64ExtensionsRequired()
|
||||
{
|
||||
// Offset MUST be considered in decision about ZIP64 format - see
|
||||
// description of Data Descriptor in ZIP File Format Specification!
|
||||
return 0xffffffff <= $this->getCompressedSize()
|
||||
|| 0xffffffff <= $this->getSize()
|
||||
|| 0xffffffff <= $this->getOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compressed size of this entry.
|
||||
*
|
||||
* @see int
|
||||
*/
|
||||
public function getCompressedSize()
|
||||
{
|
||||
return $this->compressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the compressed size of this entry.
|
||||
*
|
||||
* @param int $compressedSize The Compressed Size.
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setCompressedSize($compressedSize)
|
||||
{
|
||||
if (self::UNKNOWN != $compressedSize) {
|
||||
if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) {
|
||||
throw new ZipException("Compressed size out of range - " . $this->name);
|
||||
}
|
||||
}
|
||||
$this->compressedSize = $compressedSize;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uncompressed size of this entry.
|
||||
*
|
||||
* @see ZipEntry::setCompressedSize
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the uncompressed size of this entry.
|
||||
*
|
||||
* @param int $size The (Uncompressed) Size.
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setSize($size)
|
||||
{
|
||||
if (self::UNKNOWN != $size) {
|
||||
if (0 > $size || $size > 0x7fffffffffffffff) {
|
||||
throw new ZipException("Uncompressed Size out of range - " . $this->name);
|
||||
}
|
||||
}
|
||||
$this->size = $size;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return relative Offset Of Local File Header.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setOffset($offset)
|
||||
{
|
||||
if (0 > $offset || $offset > 0x7fffffffffffffff) {
|
||||
throw new ZipException("Offset out of range - " . $this->name);
|
||||
}
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if this ZIP entry represents a directory entry
|
||||
* (i.e. end with '/').
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDirectory()
|
||||
{
|
||||
return $this->name[strlen($this->name) - 1] === '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the General Purpose Bit Flags.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getGeneralPurposeBitFlags()
|
||||
{
|
||||
return $this->general & 0xffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the General Purpose Bit Flags.
|
||||
*
|
||||
* @var int general
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setGeneralPurposeBitFlags($general)
|
||||
{
|
||||
if (0x0000 > $general || $general > 0xffff) {
|
||||
throw new ZipException('general out of range');
|
||||
}
|
||||
$this->general = $general;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the indexed General Purpose Bit Flag.
|
||||
*
|
||||
* @param int $mask
|
||||
* @return bool
|
||||
*/
|
||||
public function getGeneralPurposeBitFlag($mask)
|
||||
{
|
||||
return 0 !== ($this->general & $mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indexed General Purpose Bit Flag.
|
||||
*
|
||||
* @param int $mask
|
||||
* @param bool $bit
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setGeneralPurposeBitFlag($mask, $bit)
|
||||
{
|
||||
if ($bit)
|
||||
$this->general |= $mask;
|
||||
else
|
||||
$this->general &= ~$mask;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if this ZIP entry is encrypted.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEncrypted()
|
||||
{
|
||||
return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encryption property to false and removes any other
|
||||
* encryption artifacts.
|
||||
*
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function clearEncryption()
|
||||
{
|
||||
$this->setEncrypted(false);
|
||||
if (null !== $this->fields) {
|
||||
$field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId());
|
||||
if (null !== $field) {
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$this->removeExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
}
|
||||
if (self::METHOD_WINZIP_AES === $this->getMethod()) {
|
||||
$this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod());
|
||||
}
|
||||
}
|
||||
$this->password = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encryption flag for this ZIP entry.
|
||||
*
|
||||
* @param bool $encrypted
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setEncrypted($encrypted)
|
||||
{
|
||||
$this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compression method for this entry.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->isInit(self::BIT_METHOD) ? $this->method & 0xffff : self::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the compression method for this entry.
|
||||
*
|
||||
* @param int $method
|
||||
* @return ZipEntry
|
||||
* @throws ZipException If method is not STORED, DEFLATED, BZIP2 or UNKNOWN.
|
||||
*/
|
||||
public function setMethod($method)
|
||||
{
|
||||
if (0x0000 > $method || $method > 0xffff) {
|
||||
throw new ZipException('method out of range');
|
||||
}
|
||||
switch ($method) {
|
||||
case self::METHOD_WINZIP_AES:
|
||||
$this->method = $method;
|
||||
$this->setInit(self::BIT_METHOD, true);
|
||||
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
|
||||
break;
|
||||
|
||||
case ZipFile::METHOD_STORED:
|
||||
case ZipFile::METHOD_DEFLATED:
|
||||
case ZipFile::METHOD_BZIP2:
|
||||
$this->method = $method;
|
||||
$this->setInit(self::BIT_METHOD, true);
|
||||
break;
|
||||
|
||||
case self::UNKNOWN:
|
||||
$this->method = ZipFile::METHOD_STORED;
|
||||
$this->setInit(self::BIT_METHOD, false);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ZipException($this->name . " (unsupported compression method $method)");
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Unix Timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
if (!$this->isInit(self::BIT_DATE_TIME)) {
|
||||
return self::UNKNOWN;
|
||||
}
|
||||
return DateTimeConverter::toUnixTimestamp($this->dosTime & 0xffffffff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time from unix timestamp.
|
||||
*
|
||||
* @param int $unixTimestamp
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setTime($unixTimestamp)
|
||||
{
|
||||
$known = self::UNKNOWN != $unixTimestamp;
|
||||
if ($known) {
|
||||
$this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
|
||||
} else {
|
||||
$this->dosTime = 0;
|
||||
}
|
||||
$this->setInit(self::BIT_DATE_TIME, $known);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external file attributes.
|
||||
*
|
||||
* @return int The external file attributes.
|
||||
*/
|
||||
public function getExternalAttributes()
|
||||
{
|
||||
if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) {
|
||||
return $this->isDirectory() ? 0x10 : 0;
|
||||
}
|
||||
return $this->externalAttributes & 0xffffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the external file attributes.
|
||||
*
|
||||
* @param int $externalAttributes the external file attributes.
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setExternalAttributes($externalAttributes)
|
||||
{
|
||||
$known = self::UNKNOWN != $externalAttributes;
|
||||
if ($known) {
|
||||
if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) {
|
||||
throw new ZipException("external file attributes out of range - " . $this->name);
|
||||
}
|
||||
$this->externalAttributes = $externalAttributes;
|
||||
} else {
|
||||
$this->externalAttributes = 0;
|
||||
}
|
||||
$this->setInit(self::BIT_EXTERNAL_ATTR, $known);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return extra field from header id.
|
||||
*
|
||||
* @param int $headerId
|
||||
* @return ExtraField|null
|
||||
*/
|
||||
public function getExtraField($headerId)
|
||||
{
|
||||
return $this->fields === null ? null : $this->fields->get($headerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extra field.
|
||||
*
|
||||
* @param ExtraField $field
|
||||
* @return ExtraField
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function addExtraField($field)
|
||||
{
|
||||
if (null === $field) {
|
||||
throw new ZipException("extra field null");
|
||||
}
|
||||
if (null === $this->fields) {
|
||||
$this->fields = new ExtraFields();
|
||||
}
|
||||
return $this->fields->add($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return exists extra field from header id.
|
||||
*
|
||||
* @param int $headerId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExtraField($headerId)
|
||||
{
|
||||
return $this->fields === null ? false : $this->fields->has($headerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove extra field from header id.
|
||||
*
|
||||
* @param int $headerId
|
||||
* @return ExtraField|null
|
||||
*/
|
||||
public function removeExtraField($headerId)
|
||||
{
|
||||
return null !== $this->fields ? $this->fields->remove($headerId) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a protective copy of the serialized Extra Fields.
|
||||
*
|
||||
* @return string A new byte array holding the serialized Extra Fields.
|
||||
* null is never returned.
|
||||
*/
|
||||
public function getExtra()
|
||||
{
|
||||
return $this->getExtraFields(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $zip64
|
||||
* @return string
|
||||
* @throws ZipException
|
||||
*/
|
||||
private function getExtraFields($zip64)
|
||||
{
|
||||
if ($zip64) {
|
||||
$field = $this->composeZip64ExtraField();
|
||||
if (null !== $field) {
|
||||
if (null === $this->fields) {
|
||||
$this->fields = new ExtraFields();
|
||||
}
|
||||
$this->fields->add($field);
|
||||
}
|
||||
} else {
|
||||
assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID));
|
||||
}
|
||||
return null === $this->fields ? null : $this->fields->getExtra();
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes a ZIP64 Extended Information Extra Field from the properties
|
||||
* of this entry.
|
||||
* If no ZIP64 Extended Information Extra Field is required it is removed
|
||||
* from the collection of Extra Fields.
|
||||
*
|
||||
* @return ExtraField|null
|
||||
*/
|
||||
private function composeZip64ExtraField()
|
||||
{
|
||||
$handle = fopen('php://memory', 'r+b');
|
||||
// Write out Uncompressed Size.
|
||||
$size = $this->getSize();
|
||||
if (0xffffffff <= $size) {
|
||||
fwrite($handle, PackUtil::packLongLE($size));
|
||||
}
|
||||
// Write out Compressed Size.
|
||||
$compressedSize = $this->getCompressedSize();
|
||||
if (0xffffffff <= $compressedSize) {
|
||||
fwrite($handle, PackUtil::packLongLE($compressedSize));
|
||||
}
|
||||
// Write out Relative Header Offset.
|
||||
$offset = $this->getOffset();
|
||||
if (0xffffffff <= $offset) {
|
||||
fwrite($handle, PackUtil::packLongLE($offset));
|
||||
}
|
||||
// Create ZIP64 Extended Information Extra Field from serialized data.
|
||||
$field = null;
|
||||
if (ftell($handle) > 0) {
|
||||
$field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID);
|
||||
$field->readFrom($handle, 0, ftell($handle));
|
||||
} else {
|
||||
$field = null;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the serialized Extra Fields by making a protective copy.
|
||||
* Note that this method parses the serialized Extra Fields according to
|
||||
* the ZIP File Format Specification and limits its size to 64 KB.
|
||||
* Therefore, this property cannot not be used to hold arbitrary
|
||||
* (application) data.
|
||||
* Consider storing such data in a separate entry instead.
|
||||
*
|
||||
* @param string $data The byte array holding the serialized Extra Fields.
|
||||
* @throws ZipException if the serialized Extra Fields exceed 64 KB
|
||||
* @return ZipEntry
|
||||
* or do not conform to the ZIP File Format Specification
|
||||
*/
|
||||
public function setExtra($data)
|
||||
{
|
||||
if (null !== $data) {
|
||||
$length = strlen($data);
|
||||
if (0x0000 > $length || $length > 0xffff) {
|
||||
throw new ZipException("Extra Fields too large");
|
||||
}
|
||||
}
|
||||
if (null === $data || strlen($data) <= 0) {
|
||||
$this->fields = null;
|
||||
} else {
|
||||
$this->setExtraFields($data, false);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param bool $zip64
|
||||
*/
|
||||
private function setExtraFields($data, $zip64)
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
$this->fields = new ExtraFields();
|
||||
}
|
||||
$handle = fopen('php://memory', 'r+b');
|
||||
fwrite($handle, $data);
|
||||
rewind($handle);
|
||||
|
||||
$this->fields->readFrom($handle, 0, strlen($data));
|
||||
$result = false;
|
||||
if ($zip64) {
|
||||
$result = $this->parseZip64ExtraField();
|
||||
}
|
||||
if ($result) {
|
||||
$this->fields->remove(ExtraField::ZIP64_HEADER_ID);
|
||||
if ($this->fields->size() <= 0) {
|
||||
if (0 !== $this->fields->size()) {
|
||||
$this->fields = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the properties of this entry from the ZIP64 Extended Information
|
||||
* Extra Field, if present.
|
||||
* The ZIP64 Extended Information Extra Field is not removed.
|
||||
*
|
||||
* @return bool
|
||||
* @throws ZipException
|
||||
*/
|
||||
private function parseZip64ExtraField()
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
return false;
|
||||
}
|
||||
$ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID);
|
||||
if (null === $ef) {
|
||||
return false;
|
||||
}
|
||||
$dataBlockHandle = $ef->getDataBlock();
|
||||
$off = 0;
|
||||
// Read in Uncompressed Size.
|
||||
$size = $this->getSize();
|
||||
if (0xffffffff <= $size) {
|
||||
assert(0xffffffff === $size);
|
||||
fseek($dataBlockHandle, $off);
|
||||
$this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
|
||||
$off += 8;
|
||||
}
|
||||
// Read in Compressed Size.
|
||||
$compressedSize = $this->getCompressedSize();
|
||||
if (0xffffffff <= $compressedSize) {
|
||||
assert(0xffffffff === $compressedSize);
|
||||
fseek($dataBlockHandle, $off);
|
||||
$this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
|
||||
$off += 8;
|
||||
}
|
||||
// Read in Relative Header Offset.
|
||||
$offset = $this->getOffset();
|
||||
if (0xffffffff <= $offset) {
|
||||
assert(0xffffffff, $offset);
|
||||
fseek($dataBlockHandle, $off);
|
||||
$this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
|
||||
//$off += 8;
|
||||
}
|
||||
fclose($dataBlockHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns comment entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return null != $this->comment ? $this->comment : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry comment.
|
||||
*
|
||||
* @param $comment
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
if (null !== $comment) {
|
||||
$commentLength = strlen($comment);
|
||||
if (0x0000 > $commentLength || $commentLength > 0xffff) {
|
||||
throw new ZipException("Comment too long");
|
||||
}
|
||||
}
|
||||
$encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true);
|
||||
if ($encoding === 'UTF-8') {
|
||||
$this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true);
|
||||
}
|
||||
$this->comment = $comment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDataDescriptorRequired()
|
||||
{
|
||||
return self::UNKNOWN == ($this->getCrc() | $this->getCompressedSize() | $this->getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return crc32 content or 0 for WinZip AES v2
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCrc()
|
||||
{
|
||||
return $this->crc & 0xffffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set crc32 content.
|
||||
*
|
||||
* @param int $crc
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setCrc($crc)
|
||||
{
|
||||
if (0x00000000 > $crc || $crc > 0xffffffff) {
|
||||
throw new ZipException("CRC-32 out of range - " . $this->name);
|
||||
}
|
||||
$this->crc = $crc;
|
||||
$this->setInit(self::BIT_CRC, true);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password and encryption method from entry
|
||||
*
|
||||
* @param string $password
|
||||
* @param null|int $encryptionMethod
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function setPassword($password, $encryptionMethod = null)
|
||||
{
|
||||
$this->password = $password;
|
||||
if ($encryptionMethod !== null) {
|
||||
$this->setEncryptionMethod($encryptionMethod);
|
||||
}
|
||||
$this->setEncrypted(!empty($this->password));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getEncryptionMethod()
|
||||
{
|
||||
return $this->encryptionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCompressionLevel()
|
||||
{
|
||||
return $this->compressionLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $compressionLevel
|
||||
* @return ZipEntry
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
|
||||
{
|
||||
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
|
||||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
|
||||
) {
|
||||
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
|
||||
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
|
||||
}
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set encryption method
|
||||
*
|
||||
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
|
||||
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
|
||||
*
|
||||
* @param int $encryptionMethod
|
||||
* @return ZipEntry
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setEncryptionMethod($encryptionMethod)
|
||||
{
|
||||
if (
|
||||
ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod &&
|
||||
ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod
|
||||
) {
|
||||
throw new ZipException('Invalid encryption method');
|
||||
}
|
||||
$this->encryptionMethod = $encryptionMethod;
|
||||
$this->setEncrypted(true);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone extra fields
|
||||
*/
|
||||
function __clone()
|
||||
{
|
||||
$this->fields = $this->fields !== null ? clone $this->fields : null;
|
||||
}
|
||||
}
|
26
src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php
Normal file
26
src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* New zip entry from empty dir.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipNewEmptyDirEntry extends ZipNewEntry
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @return null|string
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
268
src/PhpZip/Model/Entry/ZipNewEntry.php
Normal file
268
src/PhpZip/Model/Entry/ZipNewEntry.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
|
||||
use PhpZip\Crypto\WinZipAesEngine;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\PackUtil;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Abstract class for new zip entry.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class ZipNewEntry extends ZipAbstractEntry
|
||||
{
|
||||
/**
|
||||
* Default compression level for bzip2
|
||||
*/
|
||||
const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
|
||||
|
||||
/**
|
||||
* Version needed to extract.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getVersionNeededToExtract()
|
||||
{
|
||||
$method = $this->getMethod();
|
||||
return self::METHOD_WINZIP_AES === $method ? 51 :
|
||||
(ZipFile::METHOD_BZIP2 === $method ? 46 :
|
||||
($this->isZip64ExtensionsRequired() ? 45 :
|
||||
(ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write local file header, encryption header, file data and data descriptor to output stream.
|
||||
*
|
||||
* @param resource $outputStream
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function writeEntry($outputStream)
|
||||
{
|
||||
$nameLength = strlen($this->getName());
|
||||
$size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment());
|
||||
if (0xffff < $size) {
|
||||
throw new ZipException($this->getName()
|
||||
. " (the total size of "
|
||||
. $size
|
||||
. " bytes for the name, extra fields and comment exceeds the maximum size of "
|
||||
. 0xffff . " bytes)");
|
||||
}
|
||||
|
||||
if (self::UNKNOWN === $this->getPlatform()) {
|
||||
$this->setPlatform(self::PLATFORM_UNIX);
|
||||
}
|
||||
if (self::UNKNOWN === $this->getTime()) {
|
||||
$this->setTime(time());
|
||||
}
|
||||
$method = $this->getMethod();
|
||||
if (self::UNKNOWN === $method) {
|
||||
$this->setMethod($method = ZipFile::METHOD_DEFLATED);
|
||||
}
|
||||
$skipCrc = false;
|
||||
|
||||
$encrypted = $this->isEncrypted();
|
||||
$dd = $this->isDataDescriptorRequired();
|
||||
// Compose General Purpose Bit Flag.
|
||||
// See appendix D of PKWARE's ZIP File Format Specification.
|
||||
$utf8 = true;
|
||||
$general = ($encrypted ? self::GPBF_ENCRYPTED : 0)
|
||||
| ($dd ? self::GPBF_DATA_DESCRIPTOR : 0)
|
||||
| ($utf8 ? self::GPBF_UTF8 : 0);
|
||||
|
||||
$entryContent = $this->getEntryContent();
|
||||
|
||||
$this->setSize(strlen($entryContent));
|
||||
$this->setCrc(crc32($entryContent));
|
||||
|
||||
if ($encrypted && null === $this->getPassword()) {
|
||||
throw new ZipException("Can not password from entry " . $this->getName());
|
||||
}
|
||||
|
||||
if (
|
||||
$encrypted &&
|
||||
(
|
||||
self::METHOD_WINZIP_AES === $method ||
|
||||
$this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES
|
||||
)
|
||||
) {
|
||||
$field = null;
|
||||
$method = $this->getMethod();
|
||||
$keyStrength = 256; // bits
|
||||
|
||||
$compressedSize = $this->getCompressedSize();
|
||||
|
||||
if (self::METHOD_WINZIP_AES === $method) {
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
if (null !== $field) {
|
||||
$method = $field->getMethod();
|
||||
if (self::UNKNOWN !== $compressedSize) {
|
||||
$compressedSize -= $field->getKeyStrength() / 2 // salt value
|
||||
+ 2 // password verification value
|
||||
+ 10; // authentication code
|
||||
}
|
||||
$this->setMethod($method);
|
||||
}
|
||||
}
|
||||
if (null === $field) {
|
||||
$field = new WinZipAesEntryExtraField();
|
||||
}
|
||||
$field->setKeyStrength($keyStrength);
|
||||
$field->setMethod($method);
|
||||
$size = $this->getSize();
|
||||
if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) {
|
||||
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
|
||||
} else {
|
||||
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
|
||||
$skipCrc = true;
|
||||
}
|
||||
$this->addExtraField($field);
|
||||
if (self::UNKNOWN !== $compressedSize) {
|
||||
$compressedSize += $field->getKeyStrength() / 2 // salt value
|
||||
+ 2 // password verification value
|
||||
+ 10; // authentication code
|
||||
$this->setCompressedSize($compressedSize);
|
||||
}
|
||||
if ($skipCrc) {
|
||||
$this->setCrc(0);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case ZipFile::METHOD_STORED:
|
||||
break;
|
||||
case ZipFile::METHOD_DEFLATED:
|
||||
$entryContent = gzdeflate($entryContent, $this->getCompressionLevel());
|
||||
break;
|
||||
case ZipFile::METHOD_BZIP2:
|
||||
$compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
|
||||
self::LEVEL_DEFAULT_BZIP2_COMPRESSION :
|
||||
$this->getCompressionLevel();
|
||||
$entryContent = bzcompress($entryContent, $compressionLevel);
|
||||
if (is_int($entryContent)) {
|
||||
throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")");
|
||||
}
|
||||
|
||||
if ($encrypted) {
|
||||
if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) {
|
||||
if ($skipCrc) {
|
||||
$this->setCrc(0);
|
||||
}
|
||||
$this->setMethod(self::METHOD_WINZIP_AES);
|
||||
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$winZipAesEngine = new WinZipAesEngine($this, $field);
|
||||
$entryContent = $winZipAesEngine->encrypt($entryContent);
|
||||
} elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
|
||||
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
|
||||
$entryContent = $zipCryptoEngine->encrypt($entryContent);
|
||||
}
|
||||
}
|
||||
|
||||
$compressedSize = strlen($entryContent);
|
||||
$this->setCompressedSize($compressedSize);
|
||||
|
||||
$offset = ftell($outputStream);
|
||||
|
||||
// Commit changes.
|
||||
$this->setGeneralPurposeBitFlags($general);
|
||||
$this->setOffset($offset);
|
||||
|
||||
$extra = $this->getExtra();
|
||||
|
||||
// zip align
|
||||
$padding = 0;
|
||||
$zipAlign = $this->getCentralDirectory()->getZipAlign();
|
||||
$extraLength = strlen($extra);
|
||||
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
|
||||
$padding =
|
||||
(
|
||||
$zipAlign -
|
||||
(
|
||||
$offset +
|
||||
ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
|
||||
$nameLength + $extraLength
|
||||
) % $zipAlign
|
||||
) % $zipAlign;
|
||||
}
|
||||
|
||||
fwrite(
|
||||
$outputStream,
|
||||
pack(
|
||||
'VvvvVVVVvv',
|
||||
// local file header signature 4 bytes (0x04034b50)
|
||||
self::LOCAL_FILE_HEADER_SIG,
|
||||
// version needed to extract 2 bytes
|
||||
$this->getVersionNeededToExtract(),
|
||||
// general purpose bit flag 2 bytes
|
||||
$general,
|
||||
// compression method 2 bytes
|
||||
$this->getMethod(),
|
||||
// last mod file time 2 bytes
|
||||
// last mod file date 2 bytes
|
||||
$this->getTime(),
|
||||
// crc-32 4 bytes
|
||||
$dd ? 0 : $this->getCrc(),
|
||||
// compressed size 4 bytes
|
||||
$dd ? 0 : $this->getCompressedSize(),
|
||||
// uncompressed size 4 bytes
|
||||
$dd ? 0 : $this->getSize(),
|
||||
// file name length 2 bytes
|
||||
$nameLength,
|
||||
// extra field length 2 bytes
|
||||
$extraLength + $padding
|
||||
)
|
||||
);
|
||||
fwrite($outputStream, $this->getName());
|
||||
if ($extraLength > 0) {
|
||||
fwrite($outputStream, $extra);
|
||||
}
|
||||
|
||||
if ($padding > 0) {
|
||||
fwrite($outputStream, str_repeat(chr(0), $padding));
|
||||
}
|
||||
|
||||
if ($entryContent !== null) {
|
||||
fwrite($outputStream, $entryContent);
|
||||
}
|
||||
|
||||
assert(self::UNKNOWN !== $this->getCrc());
|
||||
assert(self::UNKNOWN !== $this->getSize());
|
||||
if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
|
||||
// data descriptor signature 4 bytes (0x08074b50)
|
||||
// crc-32 4 bytes
|
||||
fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc()));
|
||||
// compressed size 4 or 8 bytes
|
||||
// uncompressed size 4 or 8 bytes
|
||||
if ($this->isZip64ExtensionsRequired()) {
|
||||
fwrite($outputStream, PackUtil::packLongLE($compressedSize));
|
||||
fwrite($outputStream, PackUtil::packLongLE($this->getSize()));
|
||||
} else {
|
||||
fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize()));
|
||||
}
|
||||
} elseif ($this->getCompressedSize() !== $compressedSize) {
|
||||
throw new ZipException($this->getName()
|
||||
. " (expected compressed entry size of "
|
||||
. $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
55
src/PhpZip/Model/Entry/ZipNewStreamEntry.php
Normal file
55
src/PhpZip/Model/Entry/ZipNewStreamEntry.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Exception\InvalidArgumentException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* New zip entry from stream.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipNewStreamEntry extends ZipNewEntry
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* ZipNewStreamEntry constructor.
|
||||
* @param resource $stream
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($stream)
|
||||
{
|
||||
if (!is_resource($stream)) {
|
||||
throw new InvalidArgumentException('stream is not resource');
|
||||
}
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @return null|string
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return stream_get_contents($this->stream, -1, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release stream resource.
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
if ($this->stream !== null) {
|
||||
fclose($this->stream);
|
||||
$this->stream = null;
|
||||
}
|
||||
}
|
||||
}
|
39
src/PhpZip/Model/Entry/ZipNewStringEntry.php
Normal file
39
src/PhpZip/Model/Entry/ZipNewStringEntry.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* New zip entry from string.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipNewStringEntry extends ZipNewEntry
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $entryContent;
|
||||
|
||||
/**
|
||||
* ZipNewStringEntry constructor.
|
||||
* @param string $entryContent
|
||||
*/
|
||||
public function __construct($entryContent)
|
||||
{
|
||||
$this->entryContent = $entryContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @return null|string
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return $this->entryContent;
|
||||
}
|
||||
}
|
323
src/PhpZip/Model/Entry/ZipReadEntry.php
Normal file
323
src/PhpZip/Model/Entry/ZipReadEntry.php
Normal file
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
namespace PhpZip\Model\Entry;
|
||||
|
||||
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
|
||||
use PhpZip\Crypto\WinZipAesEngine;
|
||||
use PhpZip\Exception\Crc32Exception;
|
||||
use PhpZip\Exception\InvalidArgumentException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Exception\ZipUnsupportMethod;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Model\CentralDirectory;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* This class is used to represent a ZIP file entry.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipReadEntry extends ZipAbstractEntry
|
||||
{
|
||||
/**
|
||||
* Max size cached content in memory.
|
||||
*/
|
||||
const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 3145728; // 3 mb
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $inputStream;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $charset;
|
||||
/**
|
||||
* @var string|resource Cached entry content.
|
||||
*/
|
||||
private $entryContent;
|
||||
|
||||
/**
|
||||
* ZipFileEntry constructor.
|
||||
* @param $inputStream
|
||||
*/
|
||||
public function __construct($inputStream)
|
||||
{
|
||||
$this->inputStream = $inputStream;
|
||||
$this->readZipEntry($inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $inputStream
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function readZipEntry($inputStream)
|
||||
{
|
||||
// central file header signature 4 bytes (0x02014b50)
|
||||
$fileHeaderSig = unpack('V', fread($inputStream, 4))[1];
|
||||
if (CentralDirectory::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) {
|
||||
throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
|
||||
}
|
||||
|
||||
// version made by 2 bytes
|
||||
// version needed to extract 2 bytes
|
||||
// general purpose bit flag 2 bytes
|
||||
// compression method 2 bytes
|
||||
// last mod file time 2 bytes
|
||||
// last mod file date 2 bytes
|
||||
// crc-32 4 bytes
|
||||
// compressed size 4 bytes
|
||||
// uncompressed size 4 bytes
|
||||
// file name length 2 bytes
|
||||
// extra field length 2 bytes
|
||||
// file comment length 2 bytes
|
||||
// disk number start 2 bytes
|
||||
// internal file attributes 2 bytes
|
||||
// external file attributes 4 bytes
|
||||
// relative offset of local header 4 bytes
|
||||
$data = unpack(
|
||||
'vversionMadeBy/vversionNeededToExtract/vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
|
||||
'VrawSize/vfileLength/vextraLength/vcommentLength/VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
|
||||
fread($inputStream, 42)
|
||||
);
|
||||
|
||||
$utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
|
||||
if ($utf8) {
|
||||
$this->charset = "UTF-8";
|
||||
}
|
||||
|
||||
// See appendix D of PKWARE's ZIP File Format Specification.
|
||||
$name = fread($inputStream, $data['fileLength']);
|
||||
|
||||
$this->setName($name);
|
||||
$this->setVersionNeededToExtract($data['versionNeededToExtract']);
|
||||
$this->setPlatform($data['versionMadeBy'] >> 8);
|
||||
$this->setGeneralPurposeBitFlags($data['gpbf']);
|
||||
$this->setMethod($data['rawMethod']);
|
||||
$this->setTime($data['rawTime']);
|
||||
$this->setCrc($data['rawCrc']);
|
||||
$this->setCompressedSize($data['rawCompressedSize']);
|
||||
$this->setSize($data['rawSize']);
|
||||
$this->setExternalAttributes($data['rawExternalAttributes']);
|
||||
$this->setOffset($data['lfhOff']); // must be unmapped!
|
||||
if (0 < $data['extraLength']) {
|
||||
$this->setExtra(fread($inputStream, $data['extraLength']));
|
||||
}
|
||||
if (0 < $data['commentLength']) {
|
||||
$this->setComment(fread($inputStream, $data['commentLength']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @return string
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
if ($this->entryContent === null) {
|
||||
$isEncrypted = $this->isEncrypted();
|
||||
$password = $this->getPassword();
|
||||
if ($isEncrypted && empty($password)) {
|
||||
throw new ZipException("Not set password");
|
||||
}
|
||||
|
||||
$pos = $this->getOffset();
|
||||
assert(self::UNKNOWN !== $pos);
|
||||
$startPos = $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
|
||||
fseek($this->inputStream, $startPos);
|
||||
|
||||
// local file header signature 4 bytes (0x04034b50)
|
||||
if (self::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->inputStream, 4))[1]) {
|
||||
throw new ZipException($this->getName() . " (expected Local File Header)");
|
||||
}
|
||||
fseek($this->inputStream, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
|
||||
// file name length 2 bytes
|
||||
// extra field length 2 bytes
|
||||
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
|
||||
$pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
|
||||
|
||||
assert(self::UNKNOWN !== $this->getCrc());
|
||||
|
||||
$method = $this->getMethod();
|
||||
|
||||
fseek($this->inputStream, $pos);
|
||||
|
||||
// Get raw entry content
|
||||
$content = fread($this->inputStream, $this->getCompressedSize());
|
||||
|
||||
// Strong Encryption Specification - WinZip AES
|
||||
if ($this->isEncrypted()) {
|
||||
if (self::METHOD_WINZIP_AES === $method) {
|
||||
$winZipAesEngine = new WinZipAesEngine($this);
|
||||
$content = $winZipAesEngine->decrypt($content);
|
||||
// Disable redundant CRC-32 check.
|
||||
$isEncrypted = false;
|
||||
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$method = $field->getMethod();
|
||||
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
|
||||
} else {
|
||||
// Traditional PKWARE Decryption
|
||||
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
|
||||
$content = $zipCryptoEngine->decrypt($content);
|
||||
|
||||
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
|
||||
}
|
||||
}
|
||||
if ($isEncrypted) {
|
||||
// Check CRC32 in the Local File Header or Data Descriptor.
|
||||
$localCrc = null;
|
||||
if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
|
||||
// The CRC32 is in the Data Descriptor after the compressed size.
|
||||
// Note the Data Descriptor's Signature is optional:
|
||||
// All newer apps should write it (and so does TrueVFS),
|
||||
// but older apps might not.
|
||||
fseek($this->inputStream, $pos + $this->getCompressedSize());
|
||||
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
|
||||
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
|
||||
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
|
||||
}
|
||||
} else {
|
||||
fseek($this->inputStream, $startPos + 14);
|
||||
// The CRC32 in the Local File Header.
|
||||
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
|
||||
}
|
||||
if ($this->getCrc() !== $localCrc) {
|
||||
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case ZipFile::METHOD_STORED:
|
||||
break;
|
||||
case ZipFile::METHOD_DEFLATED:
|
||||
$content = gzinflate($content);
|
||||
break;
|
||||
case ZipFile::METHOD_BZIP2:
|
||||
if (!extension_loaded('bz2')) {
|
||||
throw new ZipException('Extension bzip2 not install');
|
||||
}
|
||||
$content = bzdecompress($content);
|
||||
break;
|
||||
default:
|
||||
throw new ZipUnsupportMethod($this->getName()
|
||||
. " (compression method "
|
||||
. $method
|
||||
. " is not supported)");
|
||||
}
|
||||
if ($isEncrypted) {
|
||||
$localCrc = crc32($content);
|
||||
if ($this->getCrc() !== $localCrc) {
|
||||
if ($this->isEncrypted()) {
|
||||
throw new ZipCryptoException("Wrong password");
|
||||
}
|
||||
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) {
|
||||
$this->entryContent = $content;
|
||||
} else {
|
||||
$this->entryContent = fopen('php://temp', 'rb');
|
||||
fwrite($this->entryContent, $content);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
if (is_resource($this->entryContent)) {
|
||||
return stream_get_contents($this->entryContent, -1, 0);
|
||||
}
|
||||
return $this->entryContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write local file header, encryption header, file data and data descriptor to output stream.
|
||||
*
|
||||
* @param resource $outputStream
|
||||
*/
|
||||
public function writeEntry($outputStream)
|
||||
{
|
||||
$pos = $this->getOffset();
|
||||
assert(ZipEntry::UNKNOWN !== $pos);
|
||||
$pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
|
||||
$pos += ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS;
|
||||
|
||||
$this->setOffset(ftell($outputStream));
|
||||
// zip align
|
||||
$padding = 0;
|
||||
$zipAlign = $this->getCentralDirectory()->getZipAlign();
|
||||
$extra = $this->getExtra();
|
||||
$extraLength = strlen($extra);
|
||||
$nameLength = strlen($this->getName());
|
||||
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
|
||||
$padding =
|
||||
(
|
||||
$zipAlign -
|
||||
($this->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength)
|
||||
% $zipAlign
|
||||
) % $zipAlign;
|
||||
}
|
||||
$dd = $this->isDataDescriptorRequired();
|
||||
|
||||
fwrite(
|
||||
$outputStream,
|
||||
pack(
|
||||
'VvvvVVVVvv',
|
||||
// local file header signature 4 bytes (0x04034b50)
|
||||
self::LOCAL_FILE_HEADER_SIG,
|
||||
// version needed to extract 2 bytes
|
||||
$this->getVersionNeededToExtract(),
|
||||
// general purpose bit flag 2 bytes
|
||||
$this->getGeneralPurposeBitFlags(),
|
||||
// compression method 2 bytes
|
||||
$this->getMethod(),
|
||||
// last mod file time 2 bytes
|
||||
// last mod file date 2 bytes
|
||||
$this->getTime(),
|
||||
// crc-32 4 bytes
|
||||
$dd ? 0 : $this->getCrc(),
|
||||
// compressed size 4 bytes
|
||||
$dd ? 0 : $this->getCompressedSize(),
|
||||
// uncompressed size 4 bytes
|
||||
$dd ? 0 : $this->getSize(),
|
||||
$nameLength,
|
||||
// extra field length 2 bytes
|
||||
$extraLength + $padding
|
||||
)
|
||||
);
|
||||
fwrite($outputStream, $this->getName());
|
||||
if ($extraLength > 0) {
|
||||
fwrite($outputStream, $extra);
|
||||
}
|
||||
|
||||
if ($padding > 0) {
|
||||
fwrite($outputStream, str_repeat(chr(0), $padding));
|
||||
}
|
||||
|
||||
fseek($this->inputStream, $pos);
|
||||
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
|
||||
fseek($this->inputStream, $data['fileLength'] + $data['extraLength'], SEEK_CUR);
|
||||
|
||||
$length = $this->getCompressedSize();
|
||||
if ($this->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
$length += 12;
|
||||
if ($this->isZip64ExtensionsRequired()) {
|
||||
$length += 8;
|
||||
}
|
||||
}
|
||||
stream_copy_to_stream($this->inputStream, $outputStream, $length);
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
{
|
||||
if ($this->entryContent !== null && is_resource($this->entryContent)) {
|
||||
fclose($this->entryContent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ namespace PhpZip\Model;
|
||||
use PhpZip\Extra\NtfsExtraField;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Util\FilesUtil;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Zip info
|
||||
@@ -85,7 +86,7 @@ class ZipInfo
|
||||
];
|
||||
|
||||
private static $valuesCompressionMethod = [
|
||||
ZipEntry::METHOD_STORED => 'no compression',
|
||||
ZipFile::METHOD_STORED => 'no compression',
|
||||
1 => 'shrink',
|
||||
2 => 'reduce level 1',
|
||||
3 => 'reduce level 2',
|
||||
@@ -93,7 +94,7 @@ class ZipInfo
|
||||
5 => 'reduce level 4',
|
||||
6 => 'implode',
|
||||
7 => 'reserved for Tokenizing compression algorithm',
|
||||
ZipEntry::METHOD_DEFLATED => 'deflate',
|
||||
ZipFile::METHOD_DEFLATED => 'deflate',
|
||||
9 => 'deflate64',
|
||||
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
|
||||
11 => 'reserved by PKWARE',
|
||||
@@ -107,7 +108,7 @@ class ZipInfo
|
||||
19 => 'IBM LZ77 z Architecture (PFS)',
|
||||
97 => 'WavPack',
|
||||
98 => 'PPMd version I, Rev 1',
|
||||
ZipEntry::WINZIP_AES => 'WinZip AES',
|
||||
ZipEntry::METHOD_WINZIP_AES => 'WinZip AES',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -214,34 +215,36 @@ class ZipInfo
|
||||
$this->platform = self::getPlatformName($entry);
|
||||
$this->version = $entry->getVersionNeededToExtract();
|
||||
|
||||
$attribs = str_repeat(" ", 12);
|
||||
$xattr = (($entry->getRawExternalAttributes() >> 16) & 0xFFFF);
|
||||
$attributes = str_repeat(" ", 12);
|
||||
$externalAttributes = $entry->getExternalAttributes();
|
||||
$xattr = (($externalAttributes >> 16) & 0xFFFF);
|
||||
switch ($entry->getPlatform()) {
|
||||
case self::MADE_BY_MS_DOS:
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case self::MADE_BY_WINDOWS_NTFS:
|
||||
if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
|
||||
($xattr & 0700) !=
|
||||
(0400 |
|
||||
(!($entry->getRawExternalAttributes() & 1) << 7) |
|
||||
(($entry->getRawExternalAttributes() & 0x10) << 2))
|
||||
(!($externalAttributes & 1) << 7) |
|
||||
(($externalAttributes & 0x10) << 2))
|
||||
) {
|
||||
$xattr = $entry->getRawExternalAttributes() & 0xFF;
|
||||
$attribs = ".r.-... ";
|
||||
$attribs[2] = ($xattr & 0x01) ? '-' : 'w';
|
||||
$attribs[5] = ($xattr & 0x02) ? 'h' : '-';
|
||||
$attribs[6] = ($xattr & 0x04) ? 's' : '-';
|
||||
$attribs[4] = ($xattr & 0x20) ? 'a' : '-';
|
||||
$xattr = $externalAttributes & 0xFF;
|
||||
$attributes = ".r.-... ";
|
||||
$attributes[2] = ($xattr & 0x01) ? '-' : 'w';
|
||||
$attributes[5] = ($xattr & 0x02) ? 'h' : '-';
|
||||
$attributes[6] = ($xattr & 0x04) ? 's' : '-';
|
||||
$attributes[4] = ($xattr & 0x20) ? 'a' : '-';
|
||||
if ($xattr & 0x10) {
|
||||
$attribs[0] = 'd';
|
||||
$attribs[3] = 'x';
|
||||
$attributes[0] = 'd';
|
||||
$attributes[3] = 'x';
|
||||
} else
|
||||
$attribs[0] = '-';
|
||||
$attributes[0] = '-';
|
||||
if ($xattr & 0x08)
|
||||
$attribs[0] = 'V';
|
||||
$attributes[0] = 'V';
|
||||
else {
|
||||
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
|
||||
$attribs[3] = 'x';
|
||||
$attributes[3] = 'x';
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -250,51 +253,51 @@ class ZipInfo
|
||||
default: /* assume Unix-like */
|
||||
switch ($xattr & self::UNX_IFMT) {
|
||||
case self::UNX_IFDIR:
|
||||
$attribs[0] = 'd';
|
||||
$attributes[0] = 'd';
|
||||
break;
|
||||
case self::UNX_IFREG:
|
||||
$attribs[0] = '-';
|
||||
$attributes[0] = '-';
|
||||
break;
|
||||
case self::UNX_IFLNK:
|
||||
$attribs[0] = 'l';
|
||||
$attributes[0] = 'l';
|
||||
break;
|
||||
case self::UNX_IFBLK:
|
||||
$attribs[0] = 'b';
|
||||
$attributes[0] = 'b';
|
||||
break;
|
||||
case self::UNX_IFCHR:
|
||||
$attribs[0] = 'c';
|
||||
$attributes[0] = 'c';
|
||||
break;
|
||||
case self::UNX_IFIFO:
|
||||
$attribs[0] = 'p';
|
||||
$attributes[0] = 'p';
|
||||
break;
|
||||
case self::UNX_IFSOCK:
|
||||
$attribs[0] = 's';
|
||||
$attributes[0] = 's';
|
||||
break;
|
||||
default:
|
||||
$attribs[0] = '?';
|
||||
$attributes[0] = '?';
|
||||
break;
|
||||
}
|
||||
$attribs[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
|
||||
$attribs[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
|
||||
$attribs[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
|
||||
$attribs[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
|
||||
$attribs[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
|
||||
$attribs[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
|
||||
$attributes[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
|
||||
$attributes[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
|
||||
$attributes[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
|
||||
$attributes[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
|
||||
$attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
|
||||
$attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
|
||||
|
||||
if ($xattr & self::UNX_IXUSR)
|
||||
$attribs[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
|
||||
$attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
|
||||
else
|
||||
$attribs[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
|
||||
$attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
|
||||
if ($xattr & self::UNX_IXGRP)
|
||||
$attribs[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
|
||||
$attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
|
||||
else
|
||||
$attribs[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
|
||||
$attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
|
||||
if ($xattr & self::UNX_IXOTH)
|
||||
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
|
||||
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
|
||||
else
|
||||
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
|
||||
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
|
||||
}
|
||||
$this->attributes = trim($attribs);
|
||||
$this->attributes = trim($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,7 +308,7 @@ class ZipInfo
|
||||
{
|
||||
$return = '';
|
||||
if ($entry->isEncrypted()) {
|
||||
if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
|
||||
if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
|
||||
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
if ($field !== null) {
|
||||
|
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
/**
|
||||
* Zip output entry for empty dir.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputEmptyDirEntry extends ZipOutputEntry
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Model\ZipEntry;
|
||||
|
||||
/**
|
||||
* Zip output Entry
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
if ($entry === null) {
|
||||
throw new \RuntimeException('entry is null');
|
||||
}
|
||||
$this->entry = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns zip entry
|
||||
*
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function getEntry()
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getEntryContent();
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Zip output entry for stream resource.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputStreamEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct($stream, ZipEntry $entry)
|
||||
{
|
||||
parent::__construct($entry);
|
||||
if (!is_resource($stream)) {
|
||||
throw new RuntimeException('stream is not resource');
|
||||
}
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
rewind($this->stream);
|
||||
return stream_get_contents($this->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release stream resource.
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
if ($this->stream !== null) {
|
||||
fclose($this->stream);
|
||||
$this->stream = null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
|
||||
/**
|
||||
* Zip output entry for string data.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputStringEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* Data content.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param ZipEntry $entry
|
||||
* @throws ZipException If data empty.
|
||||
*/
|
||||
public function __construct($data, ZipEntry $entry)
|
||||
{
|
||||
parent::__construct($entry);
|
||||
$data = (string)$data;
|
||||
if ($data === null) {
|
||||
throw new ZipException("data is null");
|
||||
}
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Zip output entry for input zip file.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputZipFileEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* Input zip file.
|
||||
*
|
||||
* @var ZipFile
|
||||
*/
|
||||
private $inputZipFile;
|
||||
|
||||
/**
|
||||
* Input entry name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $inputEntryName;
|
||||
|
||||
/**
|
||||
* ZipOutputZipFileEntry constructor.
|
||||
* @param ZipFile $zipFile
|
||||
* @param ZipEntry $zipEntry
|
||||
* @throws ZipException If input zip file is null.
|
||||
*/
|
||||
public function __construct(ZipFile $zipFile, ZipEntry $zipEntry)
|
||||
{
|
||||
if ($zipFile === null) {
|
||||
throw new ZipException('ZipFile is null');
|
||||
}
|
||||
parent::__construct(clone $zipEntry);
|
||||
|
||||
$this->inputZipFile = $zipFile;
|
||||
$this->inputEntryName = $zipEntry->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return $this->inputZipFile->getEntryContent($this->inputEntryName);
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Exception\RuntimeException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ class CryptoUtil
|
||||
*
|
||||
* @param int $length
|
||||
* @return string
|
||||
* @throws ZipException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static final function randomBytes($length)
|
||||
{
|
||||
@@ -26,7 +27,7 @@ class CryptoUtil
|
||||
} elseif (function_exists('mcrypt_create_iv')) {
|
||||
return mcrypt_create_iv($length);
|
||||
} else {
|
||||
throw new ZipException('Extension openssl or mcrypt not loaded');
|
||||
throw new RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
/**
|
||||
* Constants for ZIP files.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
interface ZipConstants
|
||||
{
|
||||
/** Local File Header signature. */
|
||||
const LOCAL_FILE_HEADER_SIG = 0x04034B50;
|
||||
|
||||
/** Data Descriptor signature. */
|
||||
const DATA_DESCRIPTOR_SIG = 0x08074B50;
|
||||
|
||||
/** Central File Header signature. */
|
||||
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
|
||||
|
||||
/** Zip64 End Of Central Directory Record. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
|
||||
|
||||
/** Zip64 End Of Central Directory Locator. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
|
||||
|
||||
/** End Of Central Directory Record signature. */
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
|
||||
|
||||
/**
|
||||
* The minimum length of the Local File Header record.
|
||||
*
|
||||
* local file header signature 4
|
||||
* version needed to extract 2
|
||||
* general purpose bit flag 2
|
||||
* compression method 2
|
||||
* last mod file time 2
|
||||
* last mod file date 2
|
||||
* crc-32 4
|
||||
* compressed size 4
|
||||
* uncompressed size 4
|
||||
* file name length 2
|
||||
* extra field length 2
|
||||
*/
|
||||
const LOCAL_FILE_HEADER_MIN_LEN = 30;
|
||||
|
||||
/**
|
||||
* The minimum length of the End Of Central Directory Record.
|
||||
*
|
||||
* end of central dir signature 4
|
||||
* number of this disk 2
|
||||
* number of the disk with the
|
||||
* start of the central directory 2
|
||||
* total number of entries in the
|
||||
* central directory on this disk 2
|
||||
* total number of entries in
|
||||
* the central directory 2
|
||||
* size of the central directory 4
|
||||
* offset of start of central *
|
||||
* directory with respect to *
|
||||
* the starting disk number 4
|
||||
* zipfile comment length 2
|
||||
*/
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
|
||||
|
||||
/**
|
||||
* The length of the Zip64 End Of Central Directory Locator.
|
||||
* zip64 end of central dir locator
|
||||
* signature 4
|
||||
* number of the disk with the
|
||||
* start of the zip64 end of
|
||||
* central directory 4
|
||||
* relative offset of the zip64
|
||||
* end of central directory record 8
|
||||
* total number of disks 4
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
|
||||
|
||||
/**
|
||||
* The minimum length of the Zip64 End Of Central Directory Record.
|
||||
*
|
||||
* zip64 end of central dir
|
||||
* signature 4
|
||||
* size of zip64 end of central
|
||||
* directory record 8
|
||||
* version made by 2
|
||||
* version needed to extract 2
|
||||
* number of this disk 4
|
||||
* number of the disk with the
|
||||
* start of the central directory 4
|
||||
* total number of entries in the
|
||||
* central directory on this disk 8
|
||||
* total number of entries in
|
||||
* the central directory 8
|
||||
* size of the central directory 8
|
||||
* offset of start of central
|
||||
* directory with respect to
|
||||
* the starting disk number 8
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
|
||||
|
||||
/**
|
||||
* Local File Header signature 4
|
||||
* Version Needed To Extract 2
|
||||
* General Purpose Bit Flags 2
|
||||
* Compression Method 2
|
||||
* Last Mod File Time 2
|
||||
* Last Mod File Date 2
|
||||
* CRC-32 4
|
||||
* Compressed Size 4
|
||||
* Uncompressed Size 4
|
||||
*/
|
||||
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1378
tests/PhpZip/ZipFileTest.php
Normal file
1378
tests/PhpZip/ZipFileTest.php
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
use PhpZip\Model\EndOfCentralDirectory;
|
||||
|
||||
/**
|
||||
* PHPUnit test case and helper methods.
|
||||
@@ -25,12 +26,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
|
||||
$output = implode(PHP_EOL, $output);
|
||||
|
||||
if ($password !== null && $returnCode === 81) {
|
||||
if(`which 7z`){
|
||||
if (`which 7z`) {
|
||||
// WinZip 99-character limit
|
||||
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
|
||||
$password = substr($password, 0, 99);
|
||||
|
||||
$command = "7z t -p" . escapeshellarg($password). " " . escapeshellarg($filename);
|
||||
$command = "7z t -p" . escapeshellarg($password) . " " . escapeshellarg($filename);
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
$output = implode(PHP_EOL, $output);
|
||||
@@ -38,14 +39,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
|
||||
self::assertEquals($returnCode, 0);
|
||||
self::assertNotContains(' Errors', $output);
|
||||
self::assertContains(' Ok', $output);
|
||||
} else {
|
||||
fwrite(STDERR, 'Program unzip cannot support this function.' . PHP_EOL);
|
||||
fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full' . PHP_EOL);
|
||||
}
|
||||
else{
|
||||
fwrite(STDERR, 'Program unzip cannot support this function.'.PHP_EOL);
|
||||
fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full'.PHP_EOL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self::assertEquals($returnCode, 0);
|
||||
} else {
|
||||
self::assertEquals($returnCode, 0, $output);
|
||||
self::assertNotContains('incorrect password', $output);
|
||||
self::assertContains(' OK', $output);
|
||||
self::assertContains('No errors', $output);
|
||||
@@ -67,18 +66,20 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
|
||||
|
||||
self::assertContains('Empty zipfile', $output);
|
||||
}
|
||||
$actualEmptyZipData = pack('VVVVVv', ZipConstants::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
|
||||
$actualEmptyZipData = pack('VVVVVv', EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
|
||||
self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param bool $showErrors
|
||||
* @return bool|null If null - can not install zipalign
|
||||
*/
|
||||
public static function doZipAlignVerify($filename)
|
||||
public static function doZipAlignVerify($filename, $showErrors = false)
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
|
||||
exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
|
||||
if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output));
|
||||
return $returnCode === 0;
|
||||
} else {
|
||||
fwrite(STDERR, 'Can not find program "zipalign" for test');
|
||||
|
Reference in New Issue
Block a user