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

40 Commits
2.0.2 ... 3.0.2

Author SHA1 Message Date
wapplay-home-linux
810b7ca741 Merge branch 'hotfix/3.0.2' 2017-10-30 23:32:11 +03:00
wapplay-home-linux
115dfd3b52 some bugs fixed for php 32-bit 2017-10-30 23:31:39 +03:00
Ne-Lexa
183274d6da remove build on hhvm 2017-06-19 00:44:00 +03:00
Ne-Lexa
f99c0278fd Merge branch 'hotfix/3.0.1' 2017-03-15 19:03:17 +03:00
Ne-Lexa
1b065c4cca fix incorrect time in archive 2017-03-15 19:03:04 +03:00
Ne-Lexa
67fc1ea7ad Merge branch 'release/3.0.0' 2017-03-15 15:35:17 +03:00
Ne-Lexa
9716976002 update changelog 2017-03-15 15:35:09 +03:00
Ne-Lexa
4ca1717979 Merge branch 'feature/3.0.0-dev' into develop 2017-03-15 15:27:19 +03:00
wapplay-home-linux
560649b1e8 Update README 2017-03-15 10:43:52 +03:00
wapplay-home-linux
3ab98532a0 Update README 2017-03-15 10:42:46 +03:00
Ne-Lexa
0dbdc0faeb README update 2017-03-13 19:59:30 +03:00
Ne-Lexa
1e4b14177a Fix bug add files from directory iterator. 2017-03-13 19:58:51 +03:00
wapplay-home-linux
eb183c9da0 update travis ci config 2017-03-13 12:12:25 +03:00
wapplay-home-linux
72ecdca941 update travis ci config 2017-03-13 12:08:03 +03:00
wapplay-home-linux
b958cb7e19 update travis ci config 2017-03-13 11:55:59 +03:00
wapplay-home-linux
e4650b7de2 add codeclimate 2017-03-13 11:39:05 +03:00
wapplay-home-linux
f6fc289102 update ci config 2017-03-13 10:49:36 +03:00
wapplay-home-linux
af4b66bb6e update ci config 2017-03-13 09:56:25 +03:00
wapplay-home-linux
bcd7949efe update ci config 2017-03-13 09:44:34 +03:00
wapplay-home-linux
3d8be5c339 update ci config 2017-03-13 09:37:16 +03:00
wapplay-home-linux
46654e3e8d update ci config 2017-03-13 09:13:19 +03:00
wapplay-home-linux
16c214b8a4 Add .travis.yml 2017-03-13 08:37:47 +03:00
wapplay-home-linux
db50dd8e46 Add tests 2017-03-13 08:33:45 +03:00
Ne-Lexa
08c890ba24 Add tests 2017-03-10 19:03:59 +03:00
wapplay-home-linux
6691858b95 remove dependencies 2017-03-10 08:30:14 +03:00
wapplay-home-linux
f802861d86 Merge ZipFile and ZipOutputFile, optimization update archive. 2017-03-10 08:23:57 +03:00
wapplay-home-linux
d8e40ee3f1 Merge branch 'release/2.2.0' 2017-03-02 00:17:06 +03:00
wapplay-home-linux
cc75f44949 Merge tag '2.2.0' into develop
Tagging version 2.2.0 2.2.0
2017-03-02 00:17:06 +03:00
wapplay-home-linux
58e9f4bf73 Easy to initialize the output zip file. 2017-03-02 00:16:09 +03:00
wapplay-home-linux
39f8616336 Merge branch 'hotfix/2.1.1' 2016-12-14 12:36:20 +03:00
wapplay-home-linux
8d140cc1a1 Merge tag '2.1.1' into develop
Tagging version 2.1.1 2.1.1
2016-12-14 12:36:20 +03:00
wapplay-home-linux
11a7c16f1b Fixed bug from issues 1
- Fixed encryption and decryption Traditional PKWARE Encryption - add casting to 32-bit integer.
- The restriction on the length of the password to 99 characters for the WinZIP AES Encryption method.
2016-12-14 12:34:59 +03:00
Ne-Lexa
f2ffdae0c2 Merge branch 'release/2.1.0' 2016-10-14 18:10:04 +03:00
Ne-Lexa
676eca7f87 Merge tag '2.1.0' into develop
Tagging version 2.1.0 2.1.0
2016-10-14 18:10:04 +03:00
Ne-Lexa
2c402157ca Setting external attributes for new zip entries. 2016-10-14 18:08:00 +03:00
Ne-Lexa
464050ff85 Merge branch 'release/2.0.3' 2016-09-26 22:31:30 +03:00
Ne-Lexa
294e3d54ef Merge tag '2.0.3' into develop
Tagging version 2.0.3 2.0.3
2016-09-26 22:31:30 +03:00
Ne-Lexa
015166d165 Fix support entry name '0' 2016-09-26 22:30:30 +03:00
Ne-Lexa
47d308605e Correcting typos in the README 2016-09-26 18:33:05 +03:00
Ne-Lexa
9370f353c6 Merge tag '2.0.2' into develop
Tagging version 2.0.2 2.0.2
2016-09-26 18:07:38 +03:00
37 changed files with 6656 additions and 4738 deletions

9
.codeclimate.yml Normal file
View File

@@ -0,0 +1,9 @@
engines:
duplication:
enabled: true
config:
languages:
- php
exclude_paths:
- tests/
- vendor/

5
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/vendor/
/vendor
*.iml
/.idea/
/.idea
/composer.lock

32
.travis.yml Normal file
View File

@@ -0,0 +1,32 @@
language: php
php:
- '5.5'
- '5.6'
- '7.0'
- '7.1'
- nightly
# cache vendor dirs
cache:
directories:
- vendor
- $HOME/.composer/cache
addons:
code_climate:
repo_token: 486a09d58d663450146c53c81c6c64938bcf3bb0b7c8ddebdc125fe97c18213a
install:
- travis_retry composer self-update && composer --version
- travis_retry composer install --prefer-dist --no-interaction
before_script:
- sudo apt-get install p7zip-full
script:
- composer validate --no-check-lock
- vendor/bin/phpunit -v -c bootstrap.xml --coverage-clover build/logs/clover.xml
after_success:
- vendor/bin/test-reporter

9
CHANGELOG.md Normal file
View File

@@ -0,0 +1,9 @@
## 3.0.0 (2017-03-15)
Merge `ZipOutputFile` with ZipFile and optimize the zip archive update.
See the update instructions in README.md.
## 2.2.0 (2017-03-02)
Features:
- create output object `ZipOutputFile` from `ZipFile` in method `ZipFile::edit()`.
- create output object `ZipOutputFile` from filename in static method `ZipOutputFile::openFromFile(string $filename)`.

667
README.md
View File

@@ -1,50 +1,143 @@
`PhpZip` Version 2
================
`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP.
`PhpZip`
========
`PhpZip` - php library for manipulating zip archives.
The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
[![Build Status](https://travis-ci.org/Ne-Lexa/php-zip.svg?branch=master)](https://travis-ci.org/Ne-Lexa/php-zip)
[![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip)
[![Total Downloads](https://poser.pugx.org/nelexa/zip/downloads)](https://packagist.org/packages/nelexa/zip)
[![Minimum PHP Version](http://img.shields.io/badge/php%2064bit-%3E%3D%205.5-8892BF.svg)](https://php.net/)
[![Test Coverage](https://codeclimate.com/github/Ne-Lexa/php-zip/badges/coverage.svg)](https://codeclimate.com/github/Ne-Lexa/php-zip/coverage)
[![License](https://poser.pugx.org/nelexa/zip/license)](https://packagist.org/packages/nelexa/zip)
The library does not require extension `php-xml` and class `ZipArchive`.
Table of contents
-----------------
- [Features](#Features)
- [Requirements](#Requirements)
- [Installation](#Installation)
- [Examples](#Examples)
- [Documentation](#Documentation)
+ [Open Zip Archive](#Documentation-Open-Zip-Archive)
+ [Get Zip Entries](#Documentation-Open-Zip-Entries)
+ [Add Zip Entries](#Documentation-Add-Zip-Entries)
+ [ZipAlign Usage](#Documentation-ZipAlign-Usage)
+ [Save Zip File or Output](#Documentation-Save-Or-Output-Entries)
+ [Close Zip Archive](#Documentation-Close-Zip-Archive)
- [Running Tests](#Running-Tests)
- [Upgrade version 2 to version 3](#Upgrade)
Requirements
------------
- `PHP` >= 5.4 (64 bit)
- Php-extension `mbstring`
### <a name="Features"></a> 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.5, 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).
### <a name="Requirements"></a> Requirements
- `PHP` >= 5.5 (64 bit)
- Optional php-extension `bzip2` for BZIP2 compression.
- Optional php-extension `openssl` or `mcrypt` for `WinZip Aes Encryption` support.
Installation
------------
`composer require nelexa/zip`
### <a name="Installation"></a> Installation
`composer require nelexa/zip:^3.0`
Documentation
-------------
#### Class `\PhpZip\ZipFile` (open, extract, info)
### <a name="Examples"></a> Examples
```php
// create new 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
### <a name="Documentation"></a> Documentation:
#### <a name="Documentation-Open-Zip-Archive"></a> Open Zip Archive
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);
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromString($stringContents);
```
Open zip archive from stream resource.
```php
$stream = fopen($filename, 'rb');
$zipFile = \PhpZip\ZipFile::openFromStream($stream);
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromStream($stream);
```
#### <a name="Documentation-Open-Zip-Entries"></a> Get Zip Entries
Get num entries.
```php
$count = $zipFile->count();
// or
$count = count($zipFile);
// or
$count = $zipFile->count();
```
Get list files.
```php
$listFiles = $zipFile->getListFiles();
// Example result:
//
// $listFiles = [
// 'info.txt',
// 'path/to/file.jpg',
// 'another path/'
// ];
```
Foreach zip entries.
Get entry contents.
```php
// $entryName = 'path/to/example-entry-name.txt';
$contents = $zipFile[$entryName];
```
Checks whether a entry exists.
```php
// $entryName = 'path/to/example-entry-name.txt';
$hasEntry = isset($zipFile[$entryName]);
```
Check whether the directory entry.
```php
// $entryName = 'path/to/';
$isDirectory = $zipFile->isDirectory($entryName);
```
Extract all files to directory.
```php
$zipFile->extractTo($directory);
```
Extract some files to directory.
```php
$extractOnlyFiles = [
"filename1",
"filename2",
"dir/dir/dir/"
];
$zipFile->extractTo($directory, $extractOnlyFiles);
```
Iterate zip entries.
```php
foreach($zipFile as $entryName => $dataContent){
echo "Entry: $entryName" . PHP_EOL;
@@ -52,7 +145,7 @@ foreach($zipFile as $entryName => $dataContent){
echo "-----------------------------" . PHP_EOL;
}
```
Iterator zip entries.
or
```php
$iterator = new \ArrayIterator($zipFile);
while ($iterator->valid())
@@ -67,57 +160,54 @@ while ($iterator->valid())
$iterator->next();
}
```
Checks whether a entry exists.
```php
$boolValue = $zipFile->hasEntry($entryName);
```
Check whether the directory entry.
```php
$boolValue = $zipFile->isDirectory($entryName);
```
Set password to all encrypted entries.
```php
$zipFile->setPassword($password);
```
Set password to concrete zip entry.
```php
$zipFile->setEntryPassword($entryName, $password);
```
Get comment archive.
```php
$commentArchive = $zipFile->getComment();
$commentArchive = $zipFile->getArchiveComment();
```
Get comment zip entry.
```php
$commentEntry = $zipFile->getEntryComment($entryName);
```
Set password for read encrypted entries.
```php
$zipFile->withReadPassword($password);
```
Get entry info.
```php
$zipInfo = $zipFile->getEntryInfo('file.txt');
echo $zipInfo . PHP_EOL;
// ZipInfo {Path="file.txt", Size=9.77KB, Compressed size=2.04KB, Modified time=2016-09-24T19:25:10+03:00, Crc=0x4b5ab5c7, Method="Deflate", Platform="UNIX", Version=20}
// Output:
// ZipInfo {Path="file.txt", Size=9.77KB, Compressed size=2.04KB, Modified time=2016-09-24T19:25:10+03:00, Crc=0x4b5ab5c7, Method="Deflate", Attributes="-rw-r--r--", Platform="UNIX", Version=20}
print_r($zipInfo);
//PhpZip\Model\ZipInfo Object
//(
// [path:PhpZip\Model\ZipInfo:private] => file.txt
// [folder:PhpZip\Model\ZipInfo:private] =>
// [size:PhpZip\Model\ZipInfo:private] => 10000
// [compressedSize:PhpZip\Model\ZipInfo:private] => 2086
// [mtime:PhpZip\Model\ZipInfo:private] => 1474734310
// [ctime:PhpZip\Model\ZipInfo:private] =>
// [atime:PhpZip\Model\ZipInfo:private] =>
// [encrypted:PhpZip\Model\ZipInfo:private] =>
// [comment:PhpZip\Model\ZipInfo:private] =>
// [crc:PhpZip\Model\ZipInfo:private] => 1264235975
// [method:PhpZip\Model\ZipInfo:private] => Deflate
// [platform:PhpZip\Model\ZipInfo:private] => UNIX
// [version:PhpZip\Model\ZipInfo:private] => 20
//)
// Output:
// PhpZip\Model\ZipInfo Object
// (
// [path:PhpZip\Model\ZipInfo:private] => file.txt
// [folder:PhpZip\Model\ZipInfo:private] =>
// [size:PhpZip\Model\ZipInfo:private] => 10000
// [compressedSize:PhpZip\Model\ZipInfo:private] => 2086
// [mtime:PhpZip\Model\ZipInfo:private] => 1474734310
// [ctime:PhpZip\Model\ZipInfo:private] =>
// [atime:PhpZip\Model\ZipInfo:private] =>
// [encrypted:PhpZip\Model\ZipInfo:private] =>
// [comment:PhpZip\Model\ZipInfo:private] =>
// [crc:PhpZip\Model\ZipInfo:private] => 1264235975
// [method:PhpZip\Model\ZipInfo:private] => Deflate
// [platform:PhpZip\Model\ZipInfo:private] => UNIX
// [version:PhpZip\Model\ZipInfo:private] => 20
// [attributes:PhpZip\Model\ZipInfo:private] => -rw-r--r--
// )
```
Get info for all entries.
```php
$zipAllInfo = $zipFile->getAllInfo();
print_r($zipAllInfo);
//Array
//(
// [file.txt] => PhpZip\Model\ZipInfo Object
@@ -132,329 +222,328 @@ print_r($zipAllInfo);
//
// ...
//)
```
Extract all files to directory.
```php
$zipFile->extractTo($directory);
```
Extract some files to directory.
```php
$extractOnlyFiles = ["filename1", "filename2", "dir/dir/dir/"];
$zipFile->extractTo($directory, $extractOnlyFiles);
```
Get entry content.
```php
$data = $zipFile->getEntryContent($entryName);
```
Close zip archive.
```php
$zipFile->close();
```
#### Class `\PhpZip\ZipOutputFile` (create, update, extract)
Create zip archive.
```php
$zipOutputFile = new \PhpZip\ZipOutputFile();
// or
$zipOutputFile = \PhpZip\ZipOutputFile::create();
```
Open zip file from update.
```php
// initial ZipFile
$zipFile = \PhpZip\ZipFile::openFromFile($filename);
// Create output stream from update zip file
$zipOutputFile = new \PhpZip\ZipOutputFile($zipFile);
// or
$zipOutputFile = \PhpZip\ZipOutputFile::openFromZipFile($zipFile);
```
Add entry from file.
#### <a name="Documentation-Add-Zip-Entries"></a> Add Zip Entries
Adding a file to the zip-archive.
```php
$zipOutputFile->addFromFile($filename); // $entryName == basename($filename);
$zipOutputFile->addFromFile($filename, $entryName);
$zipOutputFile->addFromFile($filename, $entryName, ZipEntry::METHOD_DEFLATED);
$zipOutputFile->addFromFile($filename, null, ZipEntry::METHOD_BZIP2); // $entryName == basename($filename);
// entry name is file basename.
$zipFile->addFile($filename);
// or
$zipFile->addFile($filename, null);
// with entry name
$zipFile->addFile($filename, $entryName);
// or
$zipFile[$entryName] = new \SplFileInfo($filename);
// with compression method
$zipFile->addFile($filename, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFile($filename, $entryName, ZipFile::METHOD_STORED); // No compression
$zipFile->addFile($filename, null, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add entry from string data.
```php
$zipOutputFile->addFromString($entryName, $data)
$zipOutputFile->addFromString($entryName, $data, ZipEntry::METHOD_DEFLATED)
$zipFile[$entryName] = $data;
// or
$zipFile->addFromString($entryName, $data);
// with compression method
$zipFile->addFromString($entryName, $data, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFromString($entryName, $data, ZipFile::METHOD_STORED); // No compression
$zipFile->addFromString($entryName, $data, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add entry from stream.
```php
$zipOutputFile->addFromStream($stream, $entryName)
$zipOutputFile->addFromStream($stream, $entryName, ZipEntry::METHOD_DEFLATED)
// $stream = fopen(...);
$zipFile->addFromStream($stream, $entryName);
// with compression method
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_STORED); // No compression
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add empty dir
```php
$zipOutputFile->addEmptyDir($dirName);
```
Add a directory **recursively** to the archive.
```php
$zipOutputFile->addDir($dirName);
// $dirName = "path/to/";
$zipFile->addEmptyDir($dirName);
// or
$zipOutputFile->addDir($dirName, true);
$zipFile[$dirName] = null;
```
Add all entries form string contents.
```php
$mapData = [
'file.txt' => 'file contents',
'path/to/file.txt' => 'another file contents',
'empty dir/' => null,
];
$zipFile->addAll($mapData);
```
Add a directory **not recursively** to the archive.
```php
$zipOutputFile->addDir($dirName, false);
$zipFile->addDir($dirName);
// with entry path
$localPath = "to/path/";
$zipFile->addDir($dirName, $localPath);
// with compression method for all files
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_STORED); // No compression
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a directory to the archive by path `$moveToPath`
Add a directory **recursively** to the archive.
```php
$moveToPath = 'dir/subdir/';
$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath);
$zipFile->addDirRecursive($dirName);
// with entry path
$localPath = "to/path/";
$zipFile->addDirRecursive($dirName, $localPath);
// with compression method for all files
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_STORED); // No compression
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a directory to the archive with ignoring files.
Add a files from directory iterator.
```php
$ignoreFiles = ["file_ignore.txt", "dir_ignore/sub dir ignore/"];
$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath, $ignoreFiles);
// $directoryIterator = new \DirectoryIterator($dir); // not recursive
// $directoryIterator = new \RecursiveDirectoryIterator($dir); // recursive
$zipFile->addFilesFromIterator($directoryIterator);
// with entry path
$localPath = "to/path/";
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
// or
$zipFile[$localPath] = $directoryIterator;
// with compression method for all files
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_STORED); // No compression
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a directory and set compression method.
Example add a directory to the archive with ignoring files from directory iterator.
```php
$compressionMethod = ZipEntry::METHOD_DEFLATED;
$zipOutputFile->addDir($dirName, $boolRecursive, $moveToPath, $ignoreFiles, $compressionMethod);
$ignoreFiles = [
"file_ignore.txt",
"dir_ignore/sub dir ignore/"
];
// use \DirectoryIterator for not recursive
$directoryIterator = new \RecursiveDirectoryIterator($dir);
// use IgnoreFilesFilterIterator for not recursive
$ignoreIterator = new IgnoreFilesRecursiveFilterIterator(
$directoryIterator,
$ignoreFiles
);
$zipFile->addFilesFromIterator($ignoreIterator);
```
Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern);
$zipFile->addFilesFromGlobRecursive($dir, $globPattern);
// with entry path
$localPath = "to/path/";
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath);
// with compression method for all files
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
```php
$recursive = false;
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive);
```
Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive by path `$moveToPath`.
```php
$moveToPath = 'dir/dir2/dir3';
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive = true, $moveToPath);
```
Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive and set compression method.
```php
$compressionMethod = ZipEntry::METHOD_DEFLATED;
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive, $moveToPath, $compressionMethod);
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
$zipFile->addFilesFromGlob($dir, $globPattern);
// with entry path
$localPath = "to/path/";
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath);
// with compression method for all files
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern);
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern);
// with entry path
$localPath = "to/path/";
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath);
// with compression method for all files
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
```php
$recursive = false;
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive);
```
Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive by path `$moveToPath`.
```php
$moveToPath = 'dir/dir2/dir3';
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive = true, $moveToPath);
```
Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive and set compression method.
```php
$compressionMethod = ZipEntry::METHOD_DEFLATED;
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive, $moveToPath, $compressionMethod);
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
$zipFile->addFilesFromRegex($dir, $regexPattern);
// with entry path
$localPath = "to/path/";
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath);
// with compression method for all files
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
Rename entry name.
```php
$zipOutputFile->rename($oldName, $newName);
$zipFile->rename($oldName, $newName);
```
Delete entry by name.
```php
$zipOutputFile->deleteFromName($entryName);
$zipFile->deleteFromName($entryName);
```
Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files
$zipOutputFile->deleteFromGlob($globPattern);
$zipFile->deleteFromGlob($globPattern);
```
Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression).
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files
$zipOutputFile->deleteFromRegex($regexPattern);
$zipFile->deleteFromRegex($regexPattern);
```
Delete all entries.
```php
$zipOutputFile->deleteAll();
```
Get num entries.
```php
$count = $zipOutputFile->count();
// or
$count = count($zipOutputFile);
```
Get list files.
```php
$listFiles = $zipOutputFile->getListFiles();
```
Get the compression level for entries.
```php
$compressionLevel = $zipOutputFile->getLevel();
$zipFile->deleteAll();
```
Sets the compression level for entries.
```php
// This property is only used if the effective compression method is DEFLATED or BZIP2.
// Legal values are ZipOutputFile::LEVEL_DEFAULT_COMPRESSION or range from
// ZipOutputFile::LEVEL_BEST_SPEED to ZipOutputFile::LEVEL_BEST_COMPRESSION.
$compressionMethod = ZipOutputFile::LEVEL_BEST_COMPRESSION;
$zipOutputFile->setLevel($compressionLevel);
```
Get comment archive.
```php
$commentArchive = $zipOutputFile->getComment();
// Legal values are ZipFile::LEVEL_DEFAULT_COMPRESSION or range from
// ZipFile::LEVEL_BEST_SPEED to ZipFile::LEVEL_BEST_COMPRESSION.
$compressionMethod = ZipFile::LEVEL_BEST_COMPRESSION;
$zipFile->setCompressionLevel($compressionLevel);
```
Set comment archive.
```php
$zipOutputFile->setComment($commentArchive);
```
Get comment zip entry.
```php
$commentEntry = $zipOutputFile->getEntryComment($entryName);
$zipFile->setArchiveComment($commentArchive);
```
Set comment zip entry.
```php
$zipOutputFile->setEntryComment($entryName, $entryComment);
$zipFile->setEntryComment($entryName, $entryComment);
```
Set compression method for zip entry.
Set a new password.
```php
$compressionMethod = ZipEntry::METHOD_DEFLATED;
$zipOutputMethod->setCompressionMethod($entryName, $compressionMethod);
// Support compression methods:
// ZipEntry::METHOD_STORED - no compression
// ZipEntry::METHOD_DEFLATED - deflate compression
// ZipEntry::METHOD_BZIP2 - bzip2 compression (need bz2 extension)
$zipFile->withNewPassword($password);
```
Set a password for all previously added entries.
Set a new password and encryption method.
```php
$zipOutputFile->setPassword($password);
```
Set a password and encryption method for all previously added entries.
```php
$encryptionMethod = ZipEntry::ENCRYPTION_METHOD_WINZIP_AES; // default value
$zipOutputFile->setPassword($password, $encryptionMethod);
$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES; // default value
$zipFile->withNewPassword($password, $encryptionMethod);
// Support encryption methods:
// ZipEntry::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption
// ZipEntry::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption
```
Set a password for a concrete entry.
```php
$zipOutputFile->setEntryPassword($entryName, $password);
```
Set a password and encryption method for a concrete entry.
```php
$zipOutputFile->setEntryPassword($entryName, $password, $encryptionMethod);
// Support encryption methods:
// ZipEntry::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption
// ZipEntry::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption (default value)
// ZipFile::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption
// ZipFile::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption
```
Remove password from all entries.
```php
$zipOutputFile->removePasswordAllEntries();
$zipFile->withoutPassword();
```
Remove password for concrete zip entry.
#### <a name="Documentation-ZipAlign-Usage"></a> ZipAlign Usage
Set archive alignment ([`zipalign`](https://developer.android.com/studio/command-line/zipalign.html)).
```php
$zipOutputFile->removePasswordFromEntry($entryName);
// before save or output
$zipFile->setAlign(4); // alternative command: zipalign -f -v 4 filename.zip
```
#### <a name="Documentation-Save-Or-Output-Entries"></a> Save Zip File or Output
Save archive to a file.
```php
$zipOutputFile->saveAsFile($filename);
$zipFile->saveAsFile($filename);
```
Save archive to a stream.
```php
$handle = fopen($filename, 'w+b);
$autoCloseResource = true;
$zipOutputFile->saveAsStream($handle, $autoCloseResource);
if(!$autoCloseResource){
fclose($handle);
}
// $fp = fopen($filename, 'w+b');
$zipFile->saveAsStream($fp);
```
Returns the zip archive as a string.
```php
$rawZipArchiveBytes = $zipOutputFile->outputAsString();
$rawZipArchiveBytes = $zipFile->outputAsString();
```
Output .ZIP archive as attachment and terminate.
```php
$zipOutputFile->outputAsAttachment($outputFilename);
$zipFile->outputAsAttachment($outputFilename);
// or set mime type
$zipOutputFile->outputAsAttachment($outputFilename = 'output.zip', $mimeType = 'application/zip');
$mimeType = 'application/zip'
$zipFile->outputAsAttachment($outputFilename, $mimeType);
```
Extract all files to directory.
Rewrite and reopen zip archive.
```php
$zipOutputFile->extractTo($directory);
```
Extract some files to directory.
```php
$extractOnlyFiles = ["filename1", "filename2", "dir/dir/dir/"];
$zipOutputFile->extractTo($directory, $extractOnlyFiles);
```
Get entry contents.
```php
$data = $zipOutputFile->getEntryContent($entryName);
```
Foreach zip entries.
```php
foreach($zipOutputFile as $entryName => $dataContent){
echo "Entry: $entryName" . PHP_EOL;
echo "Data: $dataContent" . PHP_EOL;
echo "-----------------------------" . PHP_EOL;
}
```
Iterator zip entries.
```php
$iterator = new \ArrayIterator($zipOutputFile);
while ($iterator->valid())
{
$entryName = $iterator->key();
$dataContent = $iterator->current();
echo "Entry: $entryName" . PHP_EOL;
echo "Data: $dataContent" . PHP_EOL;
echo "-----------------------------" . PHP_EOL;
$iterator->next();
}
```
Set zip alignment (alternate program `zipalign`).
```php
// before save or output
$zipOutputFile->setAlign(4); // alternative cmd: zipalign -f -v 4 filename.zip
$zipFile->rewrite();
```
#### <a name="Documentation-Close-Zip-Archive"></a> Close Zip Archive
Close zip archive.
```php
$zipOutputFile->close();
$zipFile->close();
```
Examples
--------
Create, open, extract and update archive.
```php
$outputFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'output.zip';
$outputDirExtract = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'extract';
if(!is_dir($outputDirExtract)){
mkdir($outputDirExtract, 0755, true);
}
$zipOutputFile = \PhpZip\ZipOutputFile::create(); // create archive
$zipOutputFile->addDir(__DIR__, true); // add this dir to archive
$zipOutputFile->saveAsFile($outputFilename); // save as file
$zipOutputFile->close(); // close output file, release all streams
$zipFile = \PhpZip\ZipFile::openFromFile($outputFilename); // open zip archive from file
$zipFile->extractTo($outputDirExtract); // extract files to dir
$zipOutputFile = \PhpZip\ZipOutputFile::openFromZipFile($zipFile); // create zip output archive for update
$zipOutputFile->deleteFromRegex('~^\.~'); // delete all hidden (Unix) files
$zipOutputFile->addFromString('dir/file.txt', 'Test file'); // add files from string contents
$zipOutputFile->saveAsFile($outputFilename); // update zip file
$zipOutputFile->close(); // close output file, release all streams
$zipFile->close(); // close input file, release all streams
```
Other examples can be found in the `tests/` folder
Running Tests
-------------
### <a name="Running-Tests"></a> Running Tests
Installing development dependencies.
```bash
vendor/bin/phpunit -v --tap -c bootstrap.xml
```
composer install --dev
```
Run tests
```bash
vendor/bin/phpunit -v -c bootstrap.xml
```
### <a name="Upgrade"></a> Upgrade version 2 to version 3
Update to the New Major Version via Composer
```json
{
"require": {
"nelexa/zip": "^3.0"
}
}
```
Next, use Composer to download new versions of the libraries:
```bash
composer update nelexa/zip
```
Update your Code to Work with the New Version:
- Class `ZipOutputFile` merged to `ZipFile` and removed.
+ `new \PhpZip\ZipOutputFile()` to `new \PhpZip\ZipFile()`
- Static initialization methods are now not static.
+ `\PhpZip\ZipFile::openFromFile($filename);` to `(new \PhpZip\ZipFile())->openFile($filename);`
+ `\PhpZip\ZipOutputFile::openFromFile($filename);` to `(new \PhpZip\ZipFile())->openFile($filename);`
+ `\PhpZip\ZipFile::openFromString($contents);` to `(new \PhpZip\ZipFile())->openFromString($contents);`
+ `\PhpZip\ZipFile::openFromStream($stream);` to `(new \PhpZip\ZipFile())->openFromStream($stream);`
+ `\PhpZip\ZipOutputFile::create()` to `new \PhpZip\ZipFile()`
+ `\PhpZip\ZipOutputFile::openFromZipFile(\PhpZip\ZipFile $zipFile)` &gt; `(new \PhpZip\ZipFile())->openFile($filename);`
- Rename methods:
+ `addFromFile` to `addFile`
+ `setLevel` to `setCompressionLevel`
+ `ZipFile::setPassword` to `ZipFile::withReadPassword`
+ `ZipOutputFile::setPassword` to `ZipFile::withNewPassword`
+ `ZipOutputFile::removePasswordAllEntries` to `ZipFile::withoutPassword`
+ `ZipOutputFile::setComment` to `ZipFile::setArchiveComment`
+ `ZipFile::getComment` to `ZipFile::getArchiveComment`
- Changed signature for methods `addDir`, `addFilesFromGlob`, `addFilesFromRegex`.
- Remove methods
+ `getLevel`
+ `setCompressionMethod`
+ `setEntryPassword`

View File

@@ -1,16 +1,18 @@
{
"name": "nelexa/zip",
"description": "Zip files CRUD. Open, create, update, extract and get info tool. Support read and write encrypted archives. Support ZIP64 ext and zip align. Alternative ZipArchive. It does not require php-zip extension.",
"description": "Zip files CRUD. Open, create, update, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, ZipAlign tool, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.",
"type": "library",
"keywords": [
"zip",
"unzip",
"archive",
"extract",
"winzip",
"zipalign"
],
"require-dev": {
"phpunit/phpunit": "4.8"
"phpunit/phpunit": "4.8",
"codeclimate/php-test-reporter": "^0.4.4"
},
"license": "MIT",
"authors": [
@@ -22,17 +24,21 @@
],
"minimum-stability": "stable",
"require": {
"php-64bit": "^5.4 || ^7.0",
"ext-mbstring": "*"
"php": "^5.5 || ^7.0"
},
"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"
}
}

View File

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

View File

@@ -2,6 +2,7 @@
namespace PhpZip\Crypto;
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
@@ -105,9 +106,26 @@ class TraditionalPkwareEncryptionEngine
private function updateKeys($charAt)
{
$this->keys[0] = self::crc32($this->keys[0], $charAt);
$this->keys[1] = ($this->keys[1] + ($this->keys[0] & 0xff)) & 4294967295;
$this->keys[1] = ($this->keys[1] * 134775813 + 1) & 4294967295;
$this->keys[2] = self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff);
$this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff);
$this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1);
$this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff));
}
/**
* Cast to int
*
* @param $i
* @return int
*/
private static function toInt($i)
{
$i = (int)($i & 0xffffffff);
if ($i > 2147483647) {
return -(-$i & 0xffffffff);
} elseif ($i < -2147483648) {
return $i & -2147483648;
}
return $i;
}
/**
@@ -137,12 +155,12 @@ 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->getDosTime() >> 8) & 0xff;
} else {
// compare against the CRC otherwise
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
}
if ($headerBytes[11] !== $checkByte) {
if ($byte !== $checkByte) {
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName());
}
@@ -170,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->getDosTime() & 0x0000ffff) << 16 :
$this->entry->getCrc();
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted.
@@ -189,11 +209,12 @@ class TraditionalPkwareEncryptionEngine
/**
* @param string $content
* @return string
* @throws ZipCryptoException
*/
private function encryptData($content)
{
if ($content === null) {
throw new \RuntimeException();
if (null === $content) {
throw new ZipCryptoException('content is null');
}
$buff = '';
foreach (unpack('C*', $content) as $val) {
@@ -206,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);

View File

@@ -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,38 +81,47 @@ 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!");
}
do {
assert($this->entry->getPassword() !== null);
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
$password = $this->entry->getPassword();
assert($password !== null);
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
// 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", $this->entry->getPassword(), $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
$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)");
if (substr($mac, 0, 10) !== $authenticationCode) {
throw new ZipAuthenticationException($this->entry->getName() .
" (authenticated WinZip AES entry content has been tampered with)");
}
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
@@ -133,7 +143,7 @@ class WinZipAesEngine
for ($i = 0; $i < $numOfBlocks; ++$i) {
for ($j = 0; $j < 16; ++$j) {
$n = ord($iv[$j]);
if (++$n === 0x100) {
if (0x100 === ++$n) {
// overflow, set this one to 0, increment next
$iv[$j] = chr(0);
} else {
@@ -157,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)
{
@@ -166,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');
}
}
@@ -177,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)
{
@@ -186,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');
}
}
@@ -202,6 +214,10 @@ class WinZipAesEngine
$password = $this->entry->getPassword();
assert($password !== null);
// WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99);
$keyStrengthBytes = 32;
$keyStrengthBits = $keyStrengthBytes * 8;

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ abstract class ExtraField implements ExtraFieldHeader
if (isset(self::getRegistry()[$headerId])) {
$extraClassName = self::getRegistry()[$headerId];
$extraField = new $extraClassName;
if ($headerId !== $extraField::getHeaderId()) {
if ($extraField::getHeaderId() !== $headerId) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
@@ -61,9 +61,9 @@ abstract class ExtraField implements ExtraFieldHeader
*/
private static function getRegistry()
{
if (self::$registry === null) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = '\PhpZip\Extra\WinZipAesEntryExtraField';
self::$registry[NtfsExtraField::getHeaderId()] = '\PhpZip\Extra\NtfsExtraField';
if (null === self::$registry) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class;
self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class;
}
return self::$registry;
}
@@ -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);

View File

@@ -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']);

View File

@@ -86,7 +86,7 @@ class NtfsExtraField extends ExtraField
fseek($handle, $off, SEEK_SET);
$unpack = unpack('vtag/vsizeAttr', fread($handle, 4));
if ($unpack['sizeAttr'] === 24) {
if (24 === $unpack['sizeAttr']) {
$tagData = fread($handle, $unpack['sizeAttr']);
$this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600;
@@ -110,7 +110,7 @@ class NtfsExtraField extends ExtraField
*/
public function writeTo($handle, $off)
{
if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) {
if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) {
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData)));
$mtimeLong = ($this->mtime + 11644473600) * 10000000;

View File

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

View File

@@ -0,0 +1,419 @@
<?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;
}
/**
* Write end of central directory.
*
* @param resource $outputStream Output stream
* @param int $centralDirectoryEntries Size entries
* @param int $centralDirectoryOffset Offset central directory
*/
public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset)
{
$position = ftell($outputStream);
$centralDirectorySize = $position - $centralDirectoryOffset;
$centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff;
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries;
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
$zip64 // ZIP64 extensions?
= $centralDirectoryEntriesZip64
|| $centralDirectorySizeZip64
|| $centralDirectoryOffsetZip64;
if ($zip64) {
// relative offset of the zip64 end of central directory record
$zip64EndOfCentralDirectoryOffset = $position;
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
// size of zip64 end of central
// directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
// version made by 2 bytes
// version needed to extract 2 bytes
// due to potential use of BZIP2 compression
// number of this disk 4 bytes
// number of the disk with the
// start of the central directory 4 bytes
fwrite($outputStream, pack('vvVV', 63, 46, 0, 0));
// total number of entries in the
// central directory on this disk 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// total number of entries in the
// central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// size of the central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize));
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset));
// zip64 extensible data sector (variable size)
//
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
// relative offset of the zip64
// end of central directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
// total number of disks 4 bytes
fwrite($outputStream, pack('V', 1));
}
$comment = $this->modified ? $this->newComment : $this->comment;
$commentLength = strlen($comment);
fwrite(
$outputStream,
pack('VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
// number of this disk 2 bytes
0,
// number of the disk with the
// start of the central directory 2 bytes
0,
// total number of entries in the
// central directory on this disk 2 bytes
$centralDirectoryEntries16,
// total number of entries in
// the central directory 2 bytes
$centralDirectoryEntries16,
// size of the central directory 4 bytes
$centralDirectorySize32,
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
$centralDirectoryOffset32,
// .ZIP file comment length 2 bytes
$commentLength
)
);
if ($commentLength > 0) {
// .ZIP file comment (variable size)
fwrite($outputStream, $comment);
}
}
}

View File

@@ -0,0 +1,953 @@
<?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');
}
$this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true);
$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) {
$compressedSize = sprintf('%u', $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) {
$size = sprintf('%u', $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)
{
$offset = sprintf('%u', $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->getDosTime());
}
/**
* Set time from unix timestamp.
*
* @param int $unixTimestamp
* @return ZipEntry
*/
public function setTime($unixTimestamp)
{
$known = self::UNKNOWN != $unixTimestamp;
if ($known) {
$this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
} else {
$this->dosTime = 0;
}
$this->setInit(self::BIT_DATE_TIME, $known);
return $this;
}
/**
* Get Dos Time
*
* @return int
*/
public function getDosTime()
{
return $this->dosTime & 0xffffffff;
}
/**
* Set Dos Time
* @param int $dosTime
* @throws ZipException
*/
public function setDosTime($dosTime)
{
$dosTime = sprintf('%u', $dosTime);
if (0x00000000 > $dosTime || $dosTime > 0xffffffff) {
throw new ZipException('DosTime out of range');
}
$this->dosTime = $dosTime;
$this->setInit(self::BIT_DATE_TIME, true);
}
/**
* 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) {
$externalAttributes = sprintf('%u', $externalAttributes);
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");
}
}
$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)
{
$crc = sprintf('%u', $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 (null !== $encryptionMethod) {
$this->setEncryptionMethod($encryptionMethod);
}
$this->setEncrypted(!empty($this->password));
return $this;
}
/**
* @return int
*/
public function getEncryptionMethod()
{
return $this->encryptionMethod;
}
/**
* @return int
*/
public function getCompressionLevel()
{
return $this->compressionLevel;
}
/**
* @param int $compressionLevel
* @return ZipEntry
* @throws InvalidArgumentException
*/
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
{
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
}
$this->compressionLevel = $compressionLevel;
return $this;
}
/**
* Set encryption method
*
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
*
* @param int $encryptionMethod
* @return ZipEntry
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod)
{
if (
ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod &&
ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod
) {
throw new ZipException('Invalid encryption method');
}
$this->encryptionMethod = $encryptionMethod;
$this->setEncrypted(true);
return $this;
}
/**
* Clone extra fields
*/
function __clone()
{
$this->fields = $this->fields !== null ? clone $this->fields : null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil;
use PhpZip\ZipFile;
/**
* Zip info
@@ -36,6 +37,31 @@ class ZipInfo
const MADE_BY_OS_X = 19;
const MADE_BY_UNKNOWN = 20;
const UNX_IFMT = 0170000; /* Unix file type mask */
const UNX_IFREG = 0100000; /* Unix regular file */
const UNX_IFSOCK = 0140000; /* Unix socket (BSD, not SysV or Amiga) */
const UNX_IFLNK = 0120000; /* Unix symbolic link (not SysV, Amiga) */
const UNX_IFBLK = 0060000; /* Unix block special (not Amiga) */
const UNX_IFDIR = 0040000; /* Unix directory */
const UNX_IFCHR = 0020000; /* Unix character special (not Amiga) */
const UNX_IFIFO = 0010000; /* Unix fifo (BCC, not MSC or Amiga) */
const UNX_ISUID = 04000; /* Unix set user id on execution */
const UNX_ISGID = 02000; /* Unix set group id on execution */
const UNX_ISVTX = 01000; /* Unix directory permissions control */
const UNX_ENFMT = self::UNX_ISGID; /* Unix record locking enforcement flag */
const UNX_IRWXU = 00700; /* Unix read, write, execute: owner */
const UNX_IRUSR = 00400; /* Unix read permission: owner */
const UNX_IWUSR = 00200; /* Unix write permission: owner */
const UNX_IXUSR = 00100; /* Unix execute permission: owner */
const UNX_IRWXG = 00070; /* Unix read, write, execute: group */
const UNX_IRGRP = 00040; /* Unix read permission: group */
const UNX_IWGRP = 00020; /* Unix write permission: group */
const UNX_IXGRP = 00010; /* Unix execute permission: group */
const UNX_IRWXO = 00007; /* Unix read, write, execute: other */
const UNX_IROTH = 00004; /* Unix read permission: other */
const UNX_IWOTH = 00002; /* Unix write permission: other */
const UNX_IXOTH = 00001; /* Unix execute permission: other */
private static $valuesMadeBy = [
self::MADE_BY_MS_DOS => 'FAT',
self::MADE_BY_AMIGA => 'Amiga',
@@ -60,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',
@@ -68,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',
@@ -82,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',
];
/**
@@ -150,6 +176,11 @@ class ZipInfo
*/
private $version;
/**
* @var string
*/
private $attributes;
/**
* ZipInfo constructor.
*
@@ -162,7 +193,7 @@ class ZipInfo
$ctime = null;
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
if ($field !== null && $field instanceof NtfsExtraField) {
if (null !== $field && $field instanceof NtfsExtraField) {
/**
* @var NtfsExtraField $field
*/
@@ -183,6 +214,90 @@ class ZipInfo
$this->method = self::getMethodName($entry);
$this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract();
$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 |
(!($externalAttributes & 1) << 7) |
(($externalAttributes & 0x10) << 2))
) {
$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) {
$attributes[0] = 'd';
$attributes[3] = 'x';
} else
$attributes[0] = '-';
if ($xattr & 0x08)
$attributes[0] = 'V';
else {
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
$attributes[3] = 'x';
}
}
break;
} /* else: fall through! */
default: /* assume Unix-like */
switch ($xattr & self::UNX_IFMT) {
case self::UNX_IFDIR:
$attributes[0] = 'd';
break;
case self::UNX_IFREG:
$attributes[0] = '-';
break;
case self::UNX_IFLNK:
$attributes[0] = 'l';
break;
case self::UNX_IFBLK:
$attributes[0] = 'b';
break;
case self::UNX_IFCHR:
$attributes[0] = 'c';
break;
case self::UNX_IFIFO:
$attributes[0] = 'p';
break;
case self::UNX_IFSOCK:
$attributes[0] = 's';
break;
default:
$attributes[0] = '?';
break;
}
$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)
$attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
else
$attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
if ($xattr & self::UNX_IXGRP)
$attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
else
$attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
if ($xattr & self::UNX_IXOTH)
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
else
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
}
$this->attributes = trim($attributes);
}
/**
@@ -193,10 +308,10 @@ 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) {
if (null !== $field) {
/**
* @var WinZipAesEntryExtraField $field
*/
@@ -245,6 +360,7 @@ class ZipInfo
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'attributes' => $this->getAttributes(),
'encrypted' => $this->isEncrypted(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
@@ -358,6 +474,14 @@ class ZipInfo
return $this->version;
}
/**
* @return string
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* @return string
*/
@@ -375,6 +499,7 @@ class ZipInfo
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
. 'Method="' . $this->getMethod() . '", '
. 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<?php
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');
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,359 @@
<?php
namespace PhpZip;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
/**
* Test add directory to zip archive.
*/
class ZipFileAddDirTest extends ZipTestCase
{
private static $files = [
'.hidden' => 'Hidden file',
'text file.txt' => 'Text file',
'Текстовый документ.txt' => 'Текстовый документ',
'empty dir/' => null,
'empty dir2/ещё пустой каталог/' => null,
'catalog/New File' => 'New Catalog File',
'catalog/New File 2' => 'New Catalog File 2',
'catalog/Empty Dir/' => null,
'category/list.txt' => 'Category list',
'category/Pictures/128x160/Car/01.jpg' => 'File 01.jpg',
'category/Pictures/128x160/Car/02.jpg' => 'File 02.jpg',
'category/Pictures/240x320/Car/01.jpg' => 'File 01.jpg',
'category/Pictures/240x320/Car/02.jpg' => 'File 02.jpg',
];
/**
* Before test
*/
protected function setUp()
{
parent::setUp();
$this->fillDirectory();
}
protected function fillDirectory()
{
foreach (self::$files as $name => $content) {
$fullName = $this->outputDirname . '/' . $name;
if ($content === null) {
if (!is_dir($fullName)) {
mkdir($fullName, 0755, true);
}
} else {
$dirname = dirname($fullName);
if (!is_dir($dirname)) {
mkdir($dirname, 0755, true);
}
file_put_contents($fullName, $content);
}
}
}
protected static function assertFilesResult(ZipFile $zipFile, array $actualResultFiles = [], $localPath = '/')
{
$localPath = rtrim($localPath, '/');
$localPath = empty($localPath) ? "" : $localPath . '/';
self::assertEquals(sizeof($zipFile), sizeof($actualResultFiles));
$actualResultFiles = array_flip($actualResultFiles);
foreach (self::$files as $file => $content) {
$zipEntryName = $localPath . $file;
if (isset($actualResultFiles[$file])) {
self::assertTrue(isset($zipFile[$zipEntryName]));
self::assertEquals($zipFile[$zipEntryName], $content);
unset($actualResultFiles[$file]);
} else {
self::assertFalse(isset($zipFile[$zipEntryName]));
}
}
self::assertEmpty($actualResultFiles);
}
public function testAddDirWithLocalPath()
{
$localPath = 'to/path';
$zipFile = new ZipFile();
$zipFile->addDir($this->outputDirname, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
], $localPath);
$zipFile->close();
}
public function testAddDirWithoutLocalPath()
{
$zipFile = new ZipFile();
$zipFile->addDir($this->outputDirname);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
]);
$zipFile->close();
}
public function testAddFilesFromIterator()
{
$localPath = 'to/project';
$directoryIterator = new \DirectoryIterator($this->outputDirname);
$zipFile = new ZipFile();
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
], $localPath);
$zipFile->close();
}
public function testAddFilesFromRecursiveIterator()
{
$localPath = 'to/project';
$directoryIterator = new \RecursiveDirectoryIterator($this->outputDirname);
$zipFile = new ZipFile();
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close();
}
public function testAddRecursiveDirWithLocalPath()
{
$localPath = 'to/path';
$zipFile = new ZipFile();
$zipFile->addDirRecursive($this->outputDirname, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close();
}
public function testAddRecursiveDirWithoutLocalPath()
{
$zipFile = new ZipFile();
$zipFile->addDirRecursive($this->outputDirname);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files));
$zipFile->close();
}
public function testAddFilesFromIteratorWithIgnoreFiles(){
$localPath = 'to/project';
$ignoreFiles = [
'Текстовый документ.txt',
'empty dir/'
];
$directoryIterator = new \DirectoryIterator($this->outputDirname);
$ignoreIterator = new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
$zipFile = new ZipFile();
$zipFile->addFilesFromIterator($ignoreIterator, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'.hidden',
'text file.txt',
], $localPath);
$zipFile->close();
}
public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){
$localPath = 'to/project';
$ignoreFiles = [
'.hidden',
'empty dir2/ещё пустой каталог/',
'list.txt',
'category/Pictures/240x320',
];
$directoryIterator = new \RecursiveDirectoryIterator($this->outputDirname);
$ignoreIterator = new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles);
$zipFile = new ZipFile();
$zipFile->addFilesFromIterator($ignoreIterator, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
'catalog/New File',
'catalog/New File 2',
'catalog/Empty Dir/',
'category/Pictures/128x160/Car/01.jpg',
'category/Pictures/128x160/Car/02.jpg',
], $localPath);
$zipFile->close();
}
/**
* Create archive and add files from glob pattern
*/
public function testAddFilesFromGlob()
{
$localPath = '/';
$zipFile = new ZipFile();
$zipFile->addFilesFromGlob($this->outputDirname, '**.{txt,jpg}', $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'text file.txt',
'Текстовый документ.txt',
], $localPath);
$zipFile->close();
}
/**
* Create archive and add recursively files from glob pattern
*/
public function testAddFilesFromGlobRecursive()
{
$localPath = '/';
$zipFile = new ZipFile();
$zipFile->addFilesFromGlobRecursive($this->outputDirname, '**.{txt,jpg}', $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'text file.txt',
'Текстовый документ.txt',
'category/list.txt',
'category/Pictures/128x160/Car/01.jpg',
'category/Pictures/128x160/Car/02.jpg',
'category/Pictures/240x320/Car/01.jpg',
'category/Pictures/240x320/Car/02.jpg',
], $localPath);
$zipFile->close();
}
/**
* Create archive and add files from regex pattern
*/
public function testAddFilesFromRegex()
{
$localPath = 'path';
$zipFile = new ZipFile();
$zipFile->addFilesFromRegex($this->outputDirname, '~\.(txt|jpe?g)$~i', $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'text file.txt',
'Текстовый документ.txt',
], $localPath);
$zipFile->close();
}
/**
* Create archive and add files recursively from regex pattern
*/
public function testAddFilesFromRegexRecursive()
{
$localPath = '/';
$zipFile = new ZipFile();
$zipFile->addFilesFromRegexRecursive($this->outputDirname, '~\.(txt|jpe?g)$~i', $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [
'text file.txt',
'Текстовый документ.txt',
'category/list.txt',
'category/Pictures/128x160/Car/01.jpg',
'category/Pictures/128x160/Car/02.jpg',
'category/Pictures/240x320/Car/01.jpg',
'category/Pictures/240x320/Car/02.jpg',
], $localPath);
$zipFile->close();
}
public function testArrayAccessAddDir()
{
$localPath = 'path/to';
$iterator = new \RecursiveDirectoryIterator($this->outputDirname);
$zipFile = new ZipFile();
$zipFile[$localPath] = $iterator;
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
self::assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close();
}
}

1839
tests/PhpZip/ZipFileTest.php Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,93 @@
<?php
namespace PhpZip;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Util\FilesUtil;
/**
* PHPUnit test case and helper methods.
*/
class ZipTestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var string
*/
protected $outputFilename;
/**
* @var string
*/
protected $outputDirname;
/**
* Before test
*/
protected function setUp()
{
parent::setUp();
$id = uniqid('phpzip');
$this->outputFilename = sys_get_temp_dir() . '/' . $id . '.zip';
$this->outputDirname = sys_get_temp_dir() . '/' . $id;
}
/**
* After test
*/
protected function tearDown()
{
parent::tearDown();
if ($this->outputFilename !== null && file_exists($this->outputFilename)) {
unlink($this->outputFilename);
}
if ($this->outputDirname !== null && is_dir($this->outputDirname)) {
FilesUtil::removeDir($this->outputDirname);
}
}
/**
* Assert correct zip archive.
*
* @param $filename
* @param string $filename
* @param string|null $password
*/
public static function assertCorrectZipArchive($filename)
public static function assertCorrectZipArchive($filename, $password = null)
{
if (DIRECTORY_SEPARATOR !== '\\' && `which zip`) {
exec("zip -T " . escapeshellarg($filename), $output, $returnCode);
if (DIRECTORY_SEPARATOR !== '\\' && `which unzip`) {
$command = "unzip";
if ($password !== null) {
$command .= " -P " . escapeshellarg($password);
}
$command .= " -t " . escapeshellarg($filename);
exec($command, $output, $returnCode);
$output = implode(PHP_EOL, $output);
self::assertEquals($returnCode, 0);
self::assertNotContains('zip error', $output);
self::assertContains(' OK', $output);
if ($password !== null && $returnCode === 81) {
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);
exec($command, $output, $returnCode);
$output = implode(PHP_EOL, $output);
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 {
self::assertEquals($returnCode, 0, $output);
self::assertNotContains('incorrect password', $output);
self::assertContains(' OK', $output);
self::assertContains('No errors', $output);
}
}
}
@@ -38,21 +105,22 @@ 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 {
echo 'Can not find program "zipalign" for test' . PHP_EOL;
fwrite(STDERR, 'Can not find program "zipalign" for test');
return null;
}