mirror of
https://github.com/Ne-Lexa/php-zip.git
synced 2025-08-16 04:06:44 +02:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d8e40ee3f1 | ||
|
58e9f4bf73 | ||
|
39f8616336 | ||
|
8d140cc1a1 | ||
|
11a7c16f1b | ||
|
f2ffdae0c2 | ||
|
676eca7f87 | ||
|
2c402157ca | ||
|
464050ff85 | ||
|
294e3d54ef | ||
|
015166d165 | ||
|
47d308605e | ||
|
951433d0b7 | ||
|
9370f353c6 | ||
|
ac20d6fbf3 | ||
|
2729d8cbec | ||
|
4c6f27c269 | ||
|
f0d90da75c | ||
|
6e5d6f48e6 | ||
|
cf7f98b7c9 | ||
|
560a94c910 | ||
|
5c23e588ff | ||
|
fe38b442a0 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/vendor/
|
||||
/vendor
|
||||
*.iml
|
||||
/.idea/
|
||||
/.idea
|
||||
/composer.lock
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 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)`.
|
573
README.md
573
README.md
@@ -1,250 +1,477 @@
|
||||
## Documentation
|
||||
`PhpZip` Version 2
|
||||
================
|
||||
`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP.
|
||||
|
||||
Create and manipulate zip archives. No use ZipArchive class and php-zip extension.
|
||||
The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
|
||||
|
||||
### class \Nelexa\Zip\ZipFile
|
||||
Initialization
|
||||
ZIP64 extensions are automatically and transparently activated when reading or writing ZIP files of more than 4 GB size.
|
||||
|
||||
The library does not require extension `php-zip` and class `ZipArchive`.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- `PHP` >= 5.4 (64 bit)
|
||||
- PHP-extension `mbstring`
|
||||
- Optional php-extension `bzip2` for BZIP2 compression.
|
||||
- Optional php-extension `openssl` or `mcrypt` for `WinZip Aes Encryption` support.
|
||||
|
||||
Installation
|
||||
------------
|
||||
`composer require nelexa/zip`
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
#### Class `\PhpZip\ZipFile` (open, extract, info)
|
||||
Open zip archive from file.
|
||||
```php
|
||||
$zip = new \Nelexa\Zip\ZipFile();
|
||||
$zipFile = \PhpZip\ZipFile::openFromFile($filename);
|
||||
```
|
||||
Create archive
|
||||
Open zip archive from data string.
|
||||
```php
|
||||
$zip->create();
|
||||
$data = file_get_contents($filename);
|
||||
$zipFile = \PhpZip\ZipFile::openFromString($data);
|
||||
```
|
||||
Open archive file
|
||||
Open zip archive from stream resource.
|
||||
```php
|
||||
$zip->open($filename);
|
||||
$stream = fopen($filename, 'rb');
|
||||
$zipFile = \PhpZip\ZipFile::openFromStream($stream);
|
||||
```
|
||||
Open archive from string
|
||||
Get num entries.
|
||||
```php
|
||||
$zip->openFromString($string)
|
||||
$count = $zipFile->count();
|
||||
// or
|
||||
$count = count($zipFile);
|
||||
```
|
||||
Set password
|
||||
Get list files.
|
||||
```php
|
||||
$zip->setPassword($password);
|
||||
$listFiles = $zipFile->getListFiles();
|
||||
```
|
||||
List files
|
||||
Foreach zip entries.
|
||||
```php
|
||||
$listFiles = $zip->getListFiles();
|
||||
foreach($zipFile as $entryName => $dataContent){
|
||||
echo "Entry: $entryName" . PHP_EOL;
|
||||
echo "Data: $dataContent" . PHP_EOL;
|
||||
echo "-----------------------------" . PHP_EOL;
|
||||
}
|
||||
```
|
||||
Get count files
|
||||
Iterator zip entries.
|
||||
```php
|
||||
$countFiles = $zip->getCountFiles();
|
||||
$iterator = new \ArrayIterator($zipFile);
|
||||
while ($iterator->valid())
|
||||
{
|
||||
$entryName = $iterator->key();
|
||||
$dataContent = $iterator->current();
|
||||
|
||||
echo "Entry: $entryName" . PHP_EOL;
|
||||
echo "Data: $dataContent" . PHP_EOL;
|
||||
echo "-----------------------------" . PHP_EOL;
|
||||
|
||||
$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();
|
||||
```
|
||||
Get comment zip entry.
|
||||
```php
|
||||
$commentEntry = $zipFile->getEntryComment($entryName);
|
||||
```
|
||||
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", 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
|
||||
// [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
|
||||
// (
|
||||
// ...
|
||||
// )
|
||||
//
|
||||
// [file2.txt] => PhpZip\Model\ZipInfo Object
|
||||
// (
|
||||
// ...
|
||||
// )
|
||||
//
|
||||
// ...
|
||||
//)
|
||||
```
|
||||
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);
|
||||
```
|
||||
Edit zip archive
|
||||
```php
|
||||
$zipOutputFile = $zipFile->edit();
|
||||
```
|
||||
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
|
||||
$filename = "file.zip";
|
||||
$zipOutputFile = \PhpZip\ZipOutputFile::openFromFile($filename);
|
||||
```
|
||||
or
|
||||
```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);
|
||||
// or
|
||||
$zipOutputFile = $zipFile->edit();
|
||||
```
|
||||
Add entry from file.
|
||||
```php
|
||||
$zipOutputFile->addFromFile($filename); // $entryName == basename($filename);
|
||||
$zipOutputFile->addFromFile($filename, $entryName);
|
||||
$zipOutputFile->addFromFile($filename, $entryName, ZipEntry::METHOD_DEFLATED);
|
||||
$zipOutputFile->addFromFile($filename, $entryName, ZipEntry::METHOD_STORED); // no compress
|
||||
$zipOutputFile->addFromFile($filename, null, ZipEntry::METHOD_BZIP2); // $entryName == basename($filename);
|
||||
```
|
||||
Add entry from string data.
|
||||
```php
|
||||
$zipOutputFile->addFromString($entryName, $data);
|
||||
$zipOutputFile->addFromString($entryName, $data, ZipEntry::METHOD_DEFLATED);
|
||||
$zipOutputFile->addFromString($entryName, $data, ZipEntry::METHOD_STORED); // no compress
|
||||
```
|
||||
Add entry from stream.
|
||||
```php
|
||||
$zipOutputFile->addFromStream($stream, $entryName);
|
||||
$zipOutputFile->addFromStream($stream, $entryName, ZipEntry::METHOD_DEFLATED);
|
||||
$zipOutputFile->addFromStream($stream, $entryName, ZipEntry::METHOD_STORED); // no compress
|
||||
```
|
||||
Add empty dir
|
||||
```php
|
||||
$zip->addEmptyDir($dirName);
|
||||
$zipOutputFile->addEmptyDir($dirName);
|
||||
```
|
||||
Add dir
|
||||
Add a directory **recursively** to the archive.
|
||||
```php
|
||||
$directory = "/tmp";
|
||||
$ignoreFiles = array("xxx.file", "xxx2.file");
|
||||
$zip->addDir($directory); // add path /tmp to /
|
||||
$zip->addDir($directory, "var/temp"); // add path /tmp to var/temp
|
||||
$zip->addDir($directory, "var/temp", $ignoreFiles); // add path /tmp to var/temp and ignore files xxx.file and xxx2.file
|
||||
$zipOutputFile->addDir($dirName);
|
||||
// or
|
||||
$zipOutputFile->addDir($dirName, true);
|
||||
```
|
||||
Add files from glob pattern
|
||||
Add a directory **not recursively** to the archive.
|
||||
```php
|
||||
$zip->addGlob("music/*.mp3"); // add all mp3 files
|
||||
$zipOutputFile->addDir($dirName, false);
|
||||
```
|
||||
Add files from regex pattern
|
||||
Add a directory to the archive by path `$moveToPath`
|
||||
```php
|
||||
$zip->addPattern("~file[0-9]+\.jpg$~", "picture/");
|
||||
$moveToPath = 'dir/subdir/';
|
||||
$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath);
|
||||
```
|
||||
Add file
|
||||
Add a directory to the archive with ignoring files.
|
||||
```php
|
||||
$zip->addFile($filename);
|
||||
$zip->addFile($filename, $localName);
|
||||
$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression
|
||||
$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED);
|
||||
$ignoreFiles = ["file_ignore.txt", "dir_ignore/sub dir ignore/"];
|
||||
$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath, $ignoreFiles);
|
||||
```
|
||||
Add file from string
|
||||
Add a directory and set compression method.
|
||||
```php
|
||||
$zip->addFromString($localName, $contents);
|
||||
$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression
|
||||
$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED);
|
||||
$compressionMethod = ZipEntry::METHOD_DEFLATED;
|
||||
$zipOutputFile->addDir($dirName, $boolRecursive, $moveToPath, $ignoreFiles, $compressionMethod);
|
||||
```
|
||||
Update timestamp for all files
|
||||
Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
|
||||
```php
|
||||
$timestamp = time(); // now time
|
||||
$zip->updateTimestamp($timestamp);
|
||||
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
|
||||
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern);
|
||||
```
|
||||
Delete files from glob pattern
|
||||
Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
|
||||
```php
|
||||
$zip->deleteGlob("*.jpg"); // remove all jpg files
|
||||
$recursive = false;
|
||||
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive);
|
||||
```
|
||||
Delete files from regex pattern
|
||||
Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive by path `$moveToPath`.
|
||||
```php
|
||||
$zip->deletePattern("~\.jpg$~i"); // remove all jpg files
|
||||
$moveToPath = 'dir/dir2/dir3';
|
||||
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive = true, $moveToPath);
|
||||
```
|
||||
Delete file from index
|
||||
Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive and set compression method.
|
||||
```php
|
||||
$zip->deleteIndex(0);
|
||||
$compressionMethod = ZipEntry::METHOD_DEFLATED;
|
||||
$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive, $moveToPath, $compressionMethod);
|
||||
```
|
||||
Delete all files
|
||||
Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
|
||||
```php
|
||||
$zip->deleteAll();
|
||||
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
|
||||
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern);
|
||||
```
|
||||
Delete from file name
|
||||
Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
|
||||
```php
|
||||
$zip->deleteName($filename);
|
||||
$recursive = false;
|
||||
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive);
|
||||
```
|
||||
Extract zip archive
|
||||
Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive by path `$moveToPath`.
|
||||
```php
|
||||
$zip->extractTo($toPath)
|
||||
$zip->extractTo($toPath, array("file1", "file2")); // extract only files file1 and file2
|
||||
$moveToPath = 'dir/dir2/dir3';
|
||||
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive = true, $moveToPath);
|
||||
```
|
||||
Get archive comment
|
||||
Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive and set compression method.
|
||||
```php
|
||||
$archiveComment = $zip->getArchiveComment();
|
||||
$compressionMethod = ZipEntry::METHOD_DEFLATED;
|
||||
$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive, $moveToPath, $compressionMethod);
|
||||
```
|
||||
Set archive comment
|
||||
Rename entry name.
|
||||
```php
|
||||
$zip->setArchiveComment($comment)
|
||||
$zipOutputFile->rename($oldName, $newName);
|
||||
```
|
||||
Get comment file from index
|
||||
Delete entry by name.
|
||||
```php
|
||||
$commentFile = $zip->getCommentIndex($index);
|
||||
$zipOutputFile->deleteFromName($entryName);
|
||||
```
|
||||
Set comment file from index
|
||||
Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
|
||||
```php
|
||||
$zip->setCommentIndex($index, $comment);
|
||||
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files
|
||||
$zipOutputFile->deleteFromGlob($globPattern);
|
||||
```
|
||||
Get comment file from filename
|
||||
Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression).
|
||||
```php
|
||||
$commentFile = $zip->getCommentName($filename);
|
||||
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files
|
||||
$zipOutputFile->deleteFromRegex($regexPattern);
|
||||
```
|
||||
Set comment file from filename
|
||||
Delete all entries.
|
||||
```php
|
||||
$zip->setCommentName($name, $comment);
|
||||
$zipOutputFile->deleteAll();
|
||||
```
|
||||
Get file content from index
|
||||
Get num entries.
|
||||
```php
|
||||
$content = $zip->getFromIndex($index);
|
||||
$count = $zipOutputFile->count();
|
||||
// or
|
||||
$count = count($zipOutputFile);
|
||||
```
|
||||
Get file content from filename
|
||||
Get list files.
|
||||
```php
|
||||
$content = $zip->getFromName($name);
|
||||
$listFiles = $zipOutputFile->getListFiles();
|
||||
```
|
||||
Get filename from index
|
||||
Get the compression level for entries.
|
||||
```php
|
||||
$filename = $zip->getNameIndex($index);
|
||||
$compressionLevel = $zipOutputFile->getLevel();
|
||||
```
|
||||
Rename file from index
|
||||
Sets the compression level for entries.
|
||||
```php
|
||||
$zip->renameIndex($index, $newFilename);
|
||||
// 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);
|
||||
```
|
||||
Rename file from filename
|
||||
Get comment archive.
|
||||
```php
|
||||
$zip->renameName($oldName, $newName);
|
||||
$commentArchive = $zipOutputFile->getComment();
|
||||
```
|
||||
Get zip entries
|
||||
Set comment archive.
|
||||
```php
|
||||
/**
|
||||
* @var \Nelexa\Zip\ZipEntry[] $zipEntries
|
||||
*/
|
||||
$zipEntries = $zip->getZipEntries();
|
||||
$zipOutputFile->setComment($commentArchive);
|
||||
```
|
||||
Get zip entry from index
|
||||
Get comment zip entry.
|
||||
```php
|
||||
/**
|
||||
* @var \Nelexa\Zip\ZipEntry $zipEntry
|
||||
*/
|
||||
$zipEntry = $zip->getZipEntryIndex($index);
|
||||
$commentEntry = $zipOutputFile->getEntryComment($entryName);
|
||||
```
|
||||
Get zip entry from filename
|
||||
Set comment zip entry.
|
||||
```php
|
||||
/**
|
||||
* @var \Nelexa\Zip\ZipEntry $zipEntry
|
||||
*/
|
||||
$zipEntry = $zip->getZipEntryName($name);
|
||||
$zipOutputFile->setEntryComment($entryName, $entryComment);
|
||||
```
|
||||
Get info from index
|
||||
Set compression method for zip entry.
|
||||
```php
|
||||
$info = $zip->statIndex($index);
|
||||
// [
|
||||
// 'name' - filename
|
||||
// 'index' - index number
|
||||
// 'crc' - crc32
|
||||
// 'size' - uncompressed size
|
||||
// 'mtime' - last modify date time
|
||||
// 'comp_size' - compressed size
|
||||
// 'comp_method' - compressed method
|
||||
// ]
|
||||
```
|
||||
Get info from name
|
||||
```php
|
||||
$info = $zip->statName($name);
|
||||
// [
|
||||
// 'name' - filename
|
||||
// 'index' - index number
|
||||
// 'crc' - crc32
|
||||
// 'size' - uncompressed size
|
||||
// 'mtime' - last modify date time
|
||||
// 'comp_size' - compressed size
|
||||
// 'comp_method' - compressed method
|
||||
// ]
|
||||
```
|
||||
Get info from all files
|
||||
```php
|
||||
$info = $zip->getExtendedListFiles();
|
||||
```
|
||||
Get output contents
|
||||
```php
|
||||
$content = $zip->output();
|
||||
```
|
||||
Save opened file
|
||||
```php
|
||||
$isSuccessSave = $zip->save();
|
||||
```
|
||||
Save file as
|
||||
```php
|
||||
$zip->saveAs($outputFile);
|
||||
```
|
||||
Close archive
|
||||
```php
|
||||
$zip->close();
|
||||
```
|
||||
$compressionMethod = ZipEntry::METHOD_DEFLATED;
|
||||
$zipOutputMethod->setCompressionMethod($entryName, $compressionMethod);
|
||||
|
||||
### Example create zip archive
|
||||
```php
|
||||
$zip = new \Nelexa\Zip\ZipFile();
|
||||
$zip->create();
|
||||
$zip->addFile("README.md");
|
||||
$zip->addFile("README.md", "folder/README");
|
||||
$zip->addFromString("folder/file.txt", "File content");
|
||||
$zip->addEmptyDir("f/o/l/d/e/r");
|
||||
$zip->setArchiveComment("Archive comment");
|
||||
$zip->setCommentIndex(0, "Comment file with index 0");
|
||||
$zip->saveAs("output.zip");
|
||||
$zip->close();
|
||||
|
||||
// $ zipinfo output.zip
|
||||
// Archive: output.zip
|
||||
// Zip file size: 912 bytes, number of entries: 4
|
||||
// -rw---- 1.0 fat 387 b- defN README.md
|
||||
// -rw---- 1.0 fat 387 b- defN folder/README
|
||||
// -rw---- 1.0 fat 12 b- defN folder/file.txt
|
||||
// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/
|
||||
// 4 files, 786 bytes uncompressed, 448 bytes compressed: 43.0%
|
||||
// Support compression methods:
|
||||
// ZipEntry::METHOD_STORED - no compression
|
||||
// ZipEntry::METHOD_DEFLATED - deflate compression
|
||||
// ZipEntry::METHOD_BZIP2 - bzip2 compression (need bz2 extension)
|
||||
```
|
||||
|
||||
### Example modification zip archive
|
||||
Set a password for all previously added entries.
|
||||
```php
|
||||
$zip = new \Nelexa\Zip\ZipFile();
|
||||
$zip->open("output.zip");
|
||||
$zip->addFromString("new-file", file_get_contents(__FILE__));
|
||||
$zip->saveAs("output2.zip");
|
||||
$zip->save();
|
||||
$zip->close();
|
||||
$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);
|
||||
|
||||
// $ zipinfo output2.zip
|
||||
// Archive: output2.zip
|
||||
// Zip file size: 1331 bytes, number of entries: 5
|
||||
// -rw---- 1.0 fat 387 b- defN README.md
|
||||
// -rw---- 1.0 fat 387 b- defN folder/README
|
||||
// -rw---- 1.0 fat 12 b- defN folder/file.txt
|
||||
// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/
|
||||
// -rw---- 1.0 fat 593 b- defN new-file
|
||||
// 5 files, 1379 bytes uncompressed, 775 bytes compressed: 43.8%
|
||||
// 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)
|
||||
```
|
||||
Remove password from all entries.
|
||||
```php
|
||||
$zipOutputFile->removePasswordAllEntries();
|
||||
```
|
||||
Remove password for concrete zip entry.
|
||||
```php
|
||||
$zipOutputFile->removePasswordFromEntry($entryName);
|
||||
```
|
||||
Save archive to a file.
|
||||
```php
|
||||
$zipOutputFile->saveAsFile($filename);
|
||||
```
|
||||
Save archive to a stream.
|
||||
```php
|
||||
$handle = fopen($filename, 'w+b');
|
||||
$autoCloseResource = true;
|
||||
$zipOutputFile->saveAsStream($handle, $autoCloseResource);
|
||||
if(!$autoCloseResource){
|
||||
fclose($handle);
|
||||
}
|
||||
```
|
||||
Returns the zip archive as a string.
|
||||
```php
|
||||
$rawZipArchiveBytes = $zipOutputFile->outputAsString();
|
||||
```
|
||||
Output .ZIP archive as attachment and terminate.
|
||||
```php
|
||||
$zipOutputFile->outputAsAttachment($outputFilename);
|
||||
// or set mime type
|
||||
$zipOutputFile->outputAsAttachment($outputFilename = 'output.zip', $mimeType = 'application/zip');
|
||||
```
|
||||
Extract all files to directory.
|
||||
```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
|
||||
```
|
||||
Close zip archive.
|
||||
```php
|
||||
$zipOutputFile->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 = $zipFile->edit(); // 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
|
||||
-------------
|
||||
```bash
|
||||
vendor/bin/phpunit -v --tap -c bootstrap.xml
|
||||
```
|
10
bootstrap.xml
Normal file
10
bootstrap.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<phpunit bootstrap="./vendor/autoload.php" colors="true">
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="PhpZip test suite">
|
||||
<directory>./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
</phpunit>
|
@@ -1,25 +1,38 @@
|
||||
{
|
||||
"name": "nelexa/zip",
|
||||
"description": "Zip create, modify and extract tool. Alternative ZipArchive.",
|
||||
"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",
|
||||
"archive",
|
||||
"extract",
|
||||
"winzip",
|
||||
"zipalign"
|
||||
],
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.5"
|
||||
"phpunit/phpunit": "4.8"
|
||||
},
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ne-Lexa",
|
||||
"email": "alexey@nelexa.ru"
|
||||
"email": "alexey@nelexa.ru",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"nelexa/buffer": "^1.0"
|
||||
"php-64bit": "^5.4 || ^7.0",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nelexa\\Zip\\": "src"
|
||||
"": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
namespace Nelexa\Zip;
|
||||
|
||||
class FilterFileIterator extends \FilterIterator
|
||||
{
|
||||
private $ignoreFiles;
|
||||
private static $ignoreAlways = array('..');
|
||||
|
||||
/**
|
||||
* @param \Iterator $iterator
|
||||
* @param array $ignoreFiles
|
||||
*/
|
||||
public function __construct(\Iterator $iterator, array $ignoreFiles)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
$this->ignoreFiles = array_merge(self::$ignoreAlways, $ignoreFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)<br/>
|
||||
* Check whether the current element of the iterator is acceptable
|
||||
* @link http://php.net/manual/en/filteriterator.accept.php
|
||||
* @return bool true if the current element is acceptable, otherwise false.
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo $value
|
||||
*/
|
||||
$value = $this->current();
|
||||
$pathName = $value->getRealPath();
|
||||
foreach ($this->ignoreFiles AS $ignoreFile) {
|
||||
if ($this->endsWith($pathName, $ignoreFile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function endsWith($haystack, $needle)
|
||||
{
|
||||
// search forward starting from end minus needle length characters
|
||||
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== FALSE);
|
||||
}
|
||||
}
|
232
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php
Normal file
232
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\CryptoUtil;
|
||||
|
||||
/**
|
||||
* Traditional PKWARE Encryption Engine.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class TraditionalPkwareEncryptionEngine
|
||||
{
|
||||
/**
|
||||
* Encryption header size
|
||||
*/
|
||||
const STD_DEC_HDR_SIZE = 12;
|
||||
|
||||
/**
|
||||
* Crc table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $CRC_TABLE = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
|
||||
];
|
||||
|
||||
/**
|
||||
* Encryption keys
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $keys = [];
|
||||
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* ZipCryptoEngine constructor.
|
||||
*
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$this->initKeys($entry->getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial keys
|
||||
*
|
||||
* @param string $password
|
||||
*/
|
||||
private function initKeys($password)
|
||||
{
|
||||
$this->keys[0] = 305419896;
|
||||
$this->keys[1] = 591751049;
|
||||
$this->keys[2] = 878082192;
|
||||
foreach (unpack('C*', $password) as $b) {
|
||||
$this->updateKeys($b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update keys.
|
||||
*
|
||||
* @param string $charAt
|
||||
*/
|
||||
private function updateKeys($charAt)
|
||||
{
|
||||
$this->keys[0] = self::crc32($this->keys[0], $charAt);
|
||||
$this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff);
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update crc.
|
||||
*
|
||||
* @param int $oldCrc
|
||||
* @param string $charAt
|
||||
* @return int
|
||||
*/
|
||||
private function crc32($oldCrc, $charAt)
|
||||
{
|
||||
return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
*/
|
||||
public function decrypt($content)
|
||||
{
|
||||
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
|
||||
foreach ($headerBytes as &$byte) {
|
||||
$byte = ($byte ^ $this->decryptByte()) & 0xff;
|
||||
$this->updateKeys($byte);
|
||||
}
|
||||
|
||||
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
// compare against the file type from extended local headers
|
||||
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
|
||||
} else {
|
||||
// compare against the CRC otherwise
|
||||
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
|
||||
}
|
||||
if ($headerBytes[11] !== $checkByte) {
|
||||
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName());
|
||||
}
|
||||
|
||||
$outputContent = "";
|
||||
foreach (unpack('C*', substr($content, self::STD_DEC_HDR_SIZE)) as $val) {
|
||||
$val = ($val ^ $this->decryptByte()) & 0xff;
|
||||
$this->updateKeys($val);
|
||||
$outputContent .= pack('c', $val);
|
||||
}
|
||||
return $outputContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt byte.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function decryptByte()
|
||||
{
|
||||
$temp = $this->keys[2] | 2;
|
||||
return (($temp * ($temp ^ 1)) >> 8) & 0xffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption data
|
||||
*
|
||||
* @param string $data
|
||||
* @param int $crc
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($data, $crc)
|
||||
{
|
||||
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
|
||||
|
||||
// Initialize again since the generated bytes were encrypted.
|
||||
$this->initKeys($this->entry->getPassword());
|
||||
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
|
||||
$headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
|
||||
|
||||
$headerBytes = $this->encryptData($headerBytes);
|
||||
return $headerBytes . $this->encryptData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
private function encryptData($content)
|
||||
{
|
||||
if ($content === null) {
|
||||
throw new \RuntimeException();
|
||||
}
|
||||
$buff = '';
|
||||
foreach (unpack('C*', $content) as $val) {
|
||||
$buff .= pack('c', $this->encryptByte($val));
|
||||
}
|
||||
return $buff;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $byte
|
||||
* @return int
|
||||
*/
|
||||
protected function encryptByte($byte)
|
||||
{
|
||||
$tempVal = $byte ^ $this->decryptByte() & 0xff;
|
||||
$this->updateKeys($byte);
|
||||
return $tempVal;
|
||||
}
|
||||
}
|
239
src/PhpZip/Crypto/WinZipAesEngine.php
Normal file
239
src/PhpZip/Crypto/WinZipAesEngine.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\CryptoUtil;
|
||||
|
||||
/**
|
||||
* WinZip Aes Encryption Engine.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class WinZipAesEngine
|
||||
{
|
||||
/**
|
||||
* The block size of the Advanced Encryption Specification (AES) Algorithm
|
||||
* in bits (AES_BLOCK_SIZE_BITS).
|
||||
*/
|
||||
const AES_BLOCK_SIZE_BITS = 128;
|
||||
const PWD_VERIFIER_BITS = 16;
|
||||
/**
|
||||
* The iteration count for the derived keys of the cipher, KLAC and MAC.
|
||||
*/
|
||||
const ITERATION_COUNT = 1000;
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* WinZipAesEngine constructor.
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt from stream resource.
|
||||
*
|
||||
* @param resource $stream Input stream resource
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
* @throws ZipCryptoException
|
||||
*/
|
||||
public function decrypt($stream)
|
||||
{
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
if (null === $field) {
|
||||
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
|
||||
}
|
||||
|
||||
$pos = ftell($stream);
|
||||
|
||||
// Get key strength.
|
||||
$keyStrengthBits = $field->getKeyStrength();
|
||||
$keyStrengthBytes = $keyStrengthBits / 8;
|
||||
|
||||
$salt = fread($stream, $keyStrengthBytes / 2);
|
||||
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8);
|
||||
|
||||
$sha1Size = 20;
|
||||
|
||||
// Init start, end and size of encrypted data.
|
||||
$endPos = $pos + $this->entry->getCompressedSize();
|
||||
$start = ftell($stream);
|
||||
$footerSize = $sha1Size / 2;
|
||||
$end = $endPos - $footerSize;
|
||||
$size = $end - $start;
|
||||
|
||||
if (0 > $size) {
|
||||
throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)");
|
||||
}
|
||||
|
||||
// Load authentication code.
|
||||
fseek($stream, $end, SEEK_SET);
|
||||
$authenticationCode = fread($stream, $footerSize);
|
||||
if (ftell($stream) !== $endPos) {
|
||||
// This should never happen unless someone is writing to the
|
||||
// end of the file concurrently!
|
||||
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
|
||||
}
|
||||
|
||||
$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);
|
||||
do {
|
||||
// Here comes the strange part about WinZip AES encryption:
|
||||
// Its unorthodox use of the Password-Based Key Derivation
|
||||
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
|
||||
// Yes, the password verifier is only a 16 bit value.
|
||||
// So we must use the MAC for password verification, too.
|
||||
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
|
||||
$key = substr($keyParam, 0, $keyStrengthBytes);
|
||||
|
||||
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
|
||||
// Verify password.
|
||||
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
|
||||
|
||||
$content = stream_get_contents($stream, $size, $start);
|
||||
$mac = hash_hmac('sha1', $content, $sha1MacParam, true);
|
||||
|
||||
if ($authenticationCode !== substr($mac, 0, 10)) {
|
||||
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)");
|
||||
}
|
||||
|
||||
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decryption or encryption AES-CTR with Segment Integer Count (SIC).
|
||||
*
|
||||
* @param bool $encrypted If true encryption else decryption
|
||||
* @param string $str Data
|
||||
* @param string $key Key
|
||||
* @param string $iv IV
|
||||
* @return string
|
||||
*/
|
||||
private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv)
|
||||
{
|
||||
$numOfBlocks = ceil(strlen($str) / 16);
|
||||
$ctrStr = '';
|
||||
for ($i = 0; $i < $numOfBlocks; ++$i) {
|
||||
for ($j = 0; $j < 16; ++$j) {
|
||||
$n = ord($iv[$j]);
|
||||
if (++$n === 0x100) {
|
||||
// overflow, set this one to 0, increment next
|
||||
$iv[$j] = chr(0);
|
||||
} else {
|
||||
// no overflow, just write incremented number back and abort
|
||||
$iv[$j] = chr($n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data = substr($str, $i * 16, 16);
|
||||
$ctrStr .= $encrypted ?
|
||||
self::encryptCtr($data, $key, $iv) :
|
||||
self::decryptCtr($data, $key, $iv);
|
||||
}
|
||||
return $ctrStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt AES-CTR.
|
||||
*
|
||||
* @param string $data Raw data
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Encrypted data
|
||||
*/
|
||||
private static function encryptCtr($data, $key, $iv)
|
||||
{
|
||||
if (extension_loaded("openssl")) {
|
||||
$numBits = strlen($key) * 8;
|
||||
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt AES-CTR.
|
||||
*
|
||||
* @param string $data Encrypted data
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Raw data
|
||||
*/
|
||||
private static function decryptCtr($data, $key, $iv)
|
||||
{
|
||||
if (extension_loaded("openssl")) {
|
||||
$numBits = strlen($key) * 8;
|
||||
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption string.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($content)
|
||||
{
|
||||
// Init key strength.
|
||||
$password = $this->entry->getPassword();
|
||||
assert($password !== null);
|
||||
|
||||
// WinZip 99-character limit
|
||||
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
|
||||
$password = substr($password, 0, 99);
|
||||
|
||||
$keyStrengthBytes = 32;
|
||||
$keyStrengthBits = $keyStrengthBytes * 8;
|
||||
|
||||
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
|
||||
|
||||
$salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
|
||||
|
||||
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
|
||||
$sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
|
||||
|
||||
// Can you believe they "forgot" the nonce in the CTR mode IV?! :-(
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
|
||||
$key = substr($keyParam, 0, $keyStrengthBytes);
|
||||
|
||||
$content = self::aesCtrSegmentIntegerCounter(true, $content, $key, $iv);
|
||||
|
||||
$mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
|
||||
|
||||
return ($salt .
|
||||
substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) .
|
||||
$content .
|
||||
substr($mac, 0, 10)
|
||||
);
|
||||
}
|
||||
}
|
70
src/PhpZip/Exception/Crc32Exception.php
Normal file
70
src/PhpZip/Exception/Crc32Exception.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown to indicate a CRC32 mismatch between the declared value in the
|
||||
* Central File Header and the Data Descriptor or between the declared value
|
||||
* and the computed value from the decompressed data.
|
||||
*
|
||||
* The exception's detail message is the name of the ZIP entry.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class Crc32Exception extends ZipException
|
||||
{
|
||||
/**
|
||||
* Expected crc.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $expectedCrc;
|
||||
|
||||
/**
|
||||
* Actual crc.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $actualCrc;
|
||||
|
||||
/**
|
||||
* Crc32Exception constructor.
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $expected
|
||||
* @param int $actual
|
||||
*/
|
||||
public function __construct($name, $expected, $actual)
|
||||
{
|
||||
parent::__construct($name
|
||||
. " (expected CRC32 value 0x"
|
||||
. dechex($expected)
|
||||
. ", but is actually 0x"
|
||||
. dechex($actual)
|
||||
. ")");
|
||||
assert($expected != $actual);
|
||||
$this->expectedCrc = $expected;
|
||||
$this->actualCrc = $actual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns expected crc.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getExpectedCrc()
|
||||
{
|
||||
return $this->expectedCrc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns actual crc.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getActualCrc()
|
||||
{
|
||||
return $this->actualCrc;
|
||||
}
|
||||
|
||||
}
|
14
src/PhpZip/Exception/IllegalArgumentException.php
Normal file
14
src/PhpZip/Exception/IllegalArgumentException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown to indicate that a method has been passed an illegal or
|
||||
* inappropriate argument.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class IllegalArgumentException extends ZipException
|
||||
{
|
||||
|
||||
}
|
13
src/PhpZip/Exception/ZipAuthenticationException.php
Normal file
13
src/PhpZip/Exception/ZipAuthenticationException.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown to indicate that an authenticated ZIP entry has been tampered with.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipAuthenticationException extends ZipCryptoException
|
||||
{
|
||||
|
||||
}
|
14
src/PhpZip/Exception/ZipCryptoException.php
Normal file
14
src/PhpZip/Exception/ZipCryptoException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if there is an issue when reading or writing an encrypted ZIP file
|
||||
* or entry.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipCryptoException extends ZipException
|
||||
{
|
||||
|
||||
}
|
14
src/PhpZip/Exception/ZipException.php
Normal file
14
src/PhpZip/Exception/ZipException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Signals that a Zip exception of some sort has occurred.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
* @see \Exception
|
||||
*/
|
||||
class ZipException extends \Exception
|
||||
{
|
||||
|
||||
}
|
14
src/PhpZip/Exception/ZipNotFoundEntry.php
Normal file
14
src/PhpZip/Exception/ZipNotFoundEntry.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if entry not found.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
* @see \Exception
|
||||
*/
|
||||
class ZipNotFoundEntry extends ZipException
|
||||
{
|
||||
|
||||
}
|
14
src/PhpZip/Exception/ZipUnsupportMethod.php
Normal file
14
src/PhpZip/Exception/ZipUnsupportMethod.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace PhpZip\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if entry unsupport compression method.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
* @see \Exception
|
||||
*/
|
||||
class ZipUnsupportMethod extends ZipException
|
||||
{
|
||||
|
||||
}
|
98
src/PhpZip/Extra/DefaultExtraField.php
Normal file
98
src/PhpZip/Extra/DefaultExtraField.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Default implementation for an Extra Field in a Local or Central Header of a
|
||||
* ZIP file.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class DefaultExtraField extends ExtraField
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private static $headerId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Constructs a new Extra Field.
|
||||
*
|
||||
* @param int $headerId an unsigned short integer (two bytes) indicating the
|
||||
* type of the Extra Field.
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function __construct($headerId)
|
||||
{
|
||||
if (0x0000 > $headerId || $headerId > 0xffff) {
|
||||
throw new ZipException('headerId out of range');
|
||||
}
|
||||
self::$headerId = $headerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Header ID (type) of this Extra Field.
|
||||
* The Header ID is an unsigned short integer (two bytes)
|
||||
* which must be constant during the life cycle of this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getHeaderId()
|
||||
{
|
||||
return self::$headerId & 0xffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Data Size of this Extra Field.
|
||||
* The Data Size is an unsigned short integer (two bytes)
|
||||
* which indicates the length of the Data Block in bytes and does not
|
||||
* include its own size in this Extra Field.
|
||||
* This property may be initialized by calling ExtraField::readFrom.
|
||||
*
|
||||
* @return int The size of the Data Block in bytes
|
||||
* or 0 if unknown.
|
||||
*/
|
||||
public function getDataSize()
|
||||
{
|
||||
return null !== $this->data ? strlen($this->data) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
* @param int $size Size
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function readFrom($handle, $off, $size)
|
||||
{
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size out of range');
|
||||
}
|
||||
if ($size > 0) {
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
$this->data = fread($handle, $size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
* @param int $off
|
||||
*/
|
||||
public function writeTo($handle, $off)
|
||||
{
|
||||
if (null !== $this->data) {
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
fwrite($handle, $this->data);
|
||||
}
|
||||
}
|
||||
}
|
120
src/PhpZip/Extra/ExtraField.php
Normal file
120
src/PhpZip/Extra/ExtraField.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Abstract base class for an Extra Field in a Local or Central Header of a
|
||||
* ZIP archive.
|
||||
* It defines the common properties of all Extra Fields and how to
|
||||
* serialize/deserialize them to/from byte arrays.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class ExtraField implements ExtraFieldHeader
|
||||
{
|
||||
/** The Header ID for a ZIP64 Extended Information Extra Field. */
|
||||
const ZIP64_HEADER_ID = 0x0001;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private static $registry;
|
||||
|
||||
/**
|
||||
* A static factory method which creates a new Extra Field based on the
|
||||
* given Header ID.
|
||||
* The returned Extra Field still requires proper initialization, for
|
||||
* example by calling ExtraField::readFrom.
|
||||
*
|
||||
* @param int $headerId An unsigned short integer (two bytes) which indicates
|
||||
* the type of the returned Extra Field.
|
||||
* @return ExtraField A new Extra Field or null if not support header id.
|
||||
* @throws ZipException If headerId is out of range.
|
||||
*/
|
||||
public static function create($headerId)
|
||||
{
|
||||
if (0x0000 > $headerId || $headerId > 0xffff) {
|
||||
throw new ZipException('headerId out of range');
|
||||
}
|
||||
|
||||
/**
|
||||
* @var ExtraField $extraField
|
||||
*/
|
||||
if (isset(self::getRegistry()[$headerId])) {
|
||||
$extraClassName = self::getRegistry()[$headerId];
|
||||
$extraField = new $extraClassName;
|
||||
if ($headerId !== $extraField::getHeaderId()) {
|
||||
throw new ZipException('Runtime error support headerId ' . $headerId);
|
||||
}
|
||||
} else {
|
||||
$extraField = new DefaultExtraField($headerId);
|
||||
}
|
||||
return $extraField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered extra field classes.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private static function getRegistry()
|
||||
{
|
||||
if (self::$registry === null) {
|
||||
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = '\PhpZip\Extra\WinZipAesEntryExtraField';
|
||||
self::$registry[NtfsExtraField::getHeaderId()] = '\PhpZip\Extra\NtfsExtraField';
|
||||
}
|
||||
return self::$registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a protective copy of the Data Block.
|
||||
*
|
||||
* @return resource
|
||||
* @throws ZipException If size data block out of range.
|
||||
*/
|
||||
public function getDataBlock()
|
||||
{
|
||||
$size = $this->getDataSize();
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size data block out of range.');
|
||||
}
|
||||
$fp = fopen('php://temp', 'r+b');
|
||||
if (0 === $size) return $fp;
|
||||
$this->writeTo($fp, 0);
|
||||
rewind($fp);
|
||||
return $fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Data Size of this Extra Field.
|
||||
* The Data Size is an unsigned short integer (two bytes)
|
||||
* which indicates the length of the Data Block in bytes and does not
|
||||
* include its own size in this Extra Field.
|
||||
* This property may be initialized by calling ExtraField::readFrom.
|
||||
*
|
||||
* @return int The size of the Data Block in bytes
|
||||
* or 0 if unknown.
|
||||
*/
|
||||
abstract public function getDataSize();
|
||||
|
||||
/**
|
||||
* Serializes a Data Block of ExtraField::getDataSize bytes to the
|
||||
* resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
*/
|
||||
abstract public function writeTo($handle, $off);
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
* @param int $size Size
|
||||
*/
|
||||
abstract public function readFrom($handle, $off, $size);
|
||||
}
|
21
src/PhpZip/Extra/ExtraFieldHeader.php
Normal file
21
src/PhpZip/Extra/ExtraFieldHeader.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
/**
|
||||
* Interface ExtraFieldHeader
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
interface ExtraFieldHeader
|
||||
{
|
||||
/**
|
||||
* Returns the Header ID (type) of this Extra Field.
|
||||
* The Header ID is an unsigned short integer (two bytes)
|
||||
* which must be constant during the life cycle of this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getHeaderId();
|
||||
|
||||
}
|
213
src/PhpZip/Extra/ExtraFields.php
Normal file
213
src/PhpZip/Extra/ExtraFields.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Represents a collection of Extra Fields as they may
|
||||
* be present at several locations in ZIP files.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ExtraFields
|
||||
{
|
||||
/**
|
||||
* The map of Extra Fields.
|
||||
* Maps from Header ID to Extra Field.
|
||||
* Must not be null, but may be empty if no Extra Fields are used.
|
||||
* The map is sorted by Header IDs in ascending order.
|
||||
*
|
||||
* @var ExtraField[]
|
||||
*/
|
||||
private $extra = [];
|
||||
|
||||
/**
|
||||
* Returns the number of Extra Fields in this collection.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function size()
|
||||
{
|
||||
return sizeof($this->extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Extra Field with the given Header ID or null
|
||||
* if no such Extra Field exists.
|
||||
*
|
||||
* @param int $headerId The requested Header ID.
|
||||
* @return ExtraField The Extra Field with the given Header ID or
|
||||
* if no such Extra Field exists.
|
||||
* @throws ZipException If headerId is out of range.
|
||||
*/
|
||||
public function get($headerId)
|
||||
{
|
||||
if (0x0000 > $headerId || $headerId > 0xffff) {
|
||||
throw new ZipException('headerId out of range');
|
||||
}
|
||||
if (isset($this->extra[$headerId])) {
|
||||
return $this->extra[$headerId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given Extra Field in this collection.
|
||||
*
|
||||
* @param ExtraField $extraField The Extra Field to store in this collection.
|
||||
* @return ExtraField The Extra Field previously associated with the Header ID of
|
||||
* of the given Extra Field or null if no such Extra Field existed.
|
||||
* @throws ZipException If headerId is out of range.
|
||||
*/
|
||||
public function add(ExtraField $extraField)
|
||||
{
|
||||
$headerId = $extraField::getHeaderId();
|
||||
if (0x0000 > $headerId || $headerId > 0xffff) {
|
||||
throw new ZipException('headerId out of range');
|
||||
}
|
||||
$this->extra[$headerId] = $extraField;
|
||||
return $extraField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Extra Field exists
|
||||
*
|
||||
* @param int $headerId The requested Header ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function has($headerId)
|
||||
{
|
||||
return isset($this->extra[$headerId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the Extra Field with the given Header ID.
|
||||
*
|
||||
* @param int $headerId The requested Header ID.
|
||||
* @return ExtraField The Extra Field with the given Header ID or null
|
||||
* if no such Extra Field exists.
|
||||
* @throws ZipException If headerId is out of range or extra field not found.
|
||||
*/
|
||||
public function remove($headerId)
|
||||
{
|
||||
if (0x0000 > $headerId || $headerId > 0xffff) {
|
||||
throw new ZipException('headerId out of range');
|
||||
}
|
||||
if (isset($this->extra[$headerId])) {
|
||||
$ef = $this->extra[$headerId];
|
||||
unset($this->extra[$headerId]);
|
||||
return $ef;
|
||||
}
|
||||
throw new ZipException('ExtraField not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a protective copy of the Extra Fields.
|
||||
* null is never returned.
|
||||
*
|
||||
* @return string
|
||||
* @throws ZipException If size out of range
|
||||
*/
|
||||
public function getExtra()
|
||||
{
|
||||
$size = $this->getExtraLength();
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size out of range');
|
||||
}
|
||||
if (0 === $size) return '';
|
||||
|
||||
$fp = fopen('php://temp', 'r+b');
|
||||
$this->writeTo($fp, 0);
|
||||
rewind($fp);
|
||||
$content = stream_get_contents($fp);
|
||||
fclose($fp);
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes required to hold the Extra Fields.
|
||||
*
|
||||
* @return int The length of the Extra Fields in bytes. May be 0.
|
||||
* @see #getExtra
|
||||
*/
|
||||
public function getExtraLength()
|
||||
{
|
||||
if (empty($this->extra)) {
|
||||
return 0;
|
||||
}
|
||||
$length = 0;
|
||||
|
||||
/**
|
||||
* @var ExtraField $extraField
|
||||
*/
|
||||
foreach ($this->extra as $extraField) {
|
||||
$length += 4 + $extraField->getDataSize();
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a list of Extra Fields of ExtraField::getExtraLength bytes to the
|
||||
* stream resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset
|
||||
*/
|
||||
private function writeTo($handle, $off)
|
||||
{
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
/**
|
||||
* @var ExtraField $ef
|
||||
*/
|
||||
foreach ($this->extra as $ef) {
|
||||
fwrite($handle, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
|
||||
$off += 4;
|
||||
fwrite($handle, $ef->writeTo($handle, $off));
|
||||
$off += $ef->getDataSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset
|
||||
* @param int $size Size
|
||||
* @throws ZipException If size out of range
|
||||
*/
|
||||
public function readFrom($handle, $off, $size)
|
||||
{
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size out of range');
|
||||
}
|
||||
$map = [];
|
||||
if (null !== $handle && 0 < $size) {
|
||||
$end = $off + $size;
|
||||
while ($off < $end) {
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
$unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
|
||||
$off += 4;
|
||||
$extraField = ExtraField::create($unpack['headerId']);
|
||||
$extraField->readFrom($handle, $off, $unpack['dataSize']);
|
||||
$off += $unpack['dataSize'];
|
||||
$map[$unpack['headerId']] = $extraField;
|
||||
}
|
||||
assert($off === $end);
|
||||
}
|
||||
$this->extra = $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* If clone extra fields.
|
||||
*/
|
||||
function __clone()
|
||||
{
|
||||
foreach ($this->extra as $k => $v) {
|
||||
$this->extra[$k] = clone $v;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
176
src/PhpZip/Extra/NtfsExtraField.php
Normal file
176
src/PhpZip/Extra/NtfsExtraField.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Util\PackUtil;
|
||||
|
||||
/**
|
||||
* NTFS Extra Field
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class NtfsExtraField extends ExtraField
|
||||
{
|
||||
|
||||
/**
|
||||
* Modify time
|
||||
*
|
||||
* @var int Unix Timestamp
|
||||
*/
|
||||
private $mtime;
|
||||
|
||||
/**
|
||||
* Access Time
|
||||
*
|
||||
* @var int Unix Timestamp
|
||||
*/
|
||||
private $atime;
|
||||
|
||||
/**
|
||||
* Create Time
|
||||
*
|
||||
* @var int Unix Time
|
||||
*/
|
||||
private $ctime;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $rawData = "";
|
||||
|
||||
/**
|
||||
* Returns the Header ID (type) of this Extra Field.
|
||||
* The Header ID is an unsigned short integer (two bytes)
|
||||
* which must be constant during the life cycle of this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getHeaderId()
|
||||
{
|
||||
return 0x000a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Data Size of this Extra Field.
|
||||
* The Data Size is an unsigned short integer (two bytes)
|
||||
* which indicates the length of the Data Block in bytes and does not
|
||||
* include its own size in this Extra Field.
|
||||
* This property may be initialized by calling ExtraField::readFrom.
|
||||
*
|
||||
* @return int The size of the Data Block in bytes
|
||||
* or 0 if unknown.
|
||||
*/
|
||||
public function getDataSize()
|
||||
{
|
||||
return 8 * 4 + strlen($this->rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
* @param int $size Size
|
||||
* @throws ZipException If size out of range
|
||||
*/
|
||||
public function readFrom($handle, $off, $size)
|
||||
{
|
||||
if (0x0000 > $size || $size > 0xffff) {
|
||||
throw new ZipException('size out of range');
|
||||
}
|
||||
if ($size > 0) {
|
||||
$off += 4;
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
|
||||
$unpack = unpack('vtag/vsizeAttr', fread($handle, 4));
|
||||
if ($unpack['sizeAttr'] === 24) {
|
||||
$tagData = fread($handle, $unpack['sizeAttr']);
|
||||
|
||||
$this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600;
|
||||
$this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600;
|
||||
$this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600;
|
||||
}
|
||||
$off += $unpack['sizeAttr'];
|
||||
|
||||
if ($size > $off) {
|
||||
$this->rawData .= fread($handle, $size - $off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Data Block of ExtraField::getDataSize bytes to the
|
||||
* resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
*/
|
||||
public function writeTo($handle, $off)
|
||||
{
|
||||
if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) {
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData)));
|
||||
$mtimeLong = ($this->mtime + 11644473600) * 10000000;
|
||||
fwrite($handle, PackUtil::packLongLE($mtimeLong));
|
||||
$atimeLong = ($this->atime + 11644473600) * 10000000;
|
||||
fwrite($handle, PackUtil::packLongLE($atimeLong));
|
||||
$ctimeLong = ($this->ctime + 11644473600) * 10000000;
|
||||
fwrite($handle, PackUtil::packLongLE($ctimeLong));
|
||||
if (!empty($this->rawData)) {
|
||||
fwrite($handle, $this->rawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMtime()
|
||||
{
|
||||
return $this->mtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $mtime
|
||||
*/
|
||||
public function setMtime($mtime)
|
||||
{
|
||||
$this->mtime = (int)$mtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getAtime()
|
||||
{
|
||||
return $this->atime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $atime
|
||||
*/
|
||||
public function setAtime($atime)
|
||||
{
|
||||
$this->atime = (int)$atime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCtime()
|
||||
{
|
||||
return $this->ctime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $ctime
|
||||
*/
|
||||
public function setCtime($ctime)
|
||||
{
|
||||
$this->ctime = (int)$ctime;
|
||||
}
|
||||
|
||||
}
|
236
src/PhpZip/Extra/WinZipAesEntryExtraField.php
Normal file
236
src/PhpZip/Extra/WinZipAesEntryExtraField.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
namespace PhpZip\Extra;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* WinZip AES Extra Field.
|
||||
*
|
||||
* @see http://www.winzip.com/win/en/aes_info.htm AES Encryption Information: Encryption Specification AE-1 and AE-2 (WinZip Computing, S.L.)
|
||||
* @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers (WinZip Computing, S.L.)
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class WinZipAesEntryExtraField extends ExtraField
|
||||
{
|
||||
const DATA_SIZE = 7;
|
||||
const VENDOR_ID = 17729; // 'A' | ('E' << 8);
|
||||
|
||||
/**
|
||||
* Entries of this type <em>do</em> include the standard ZIP CRC-32 value.
|
||||
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
|
||||
*/
|
||||
const VV_AE_1 = 1;
|
||||
|
||||
/**
|
||||
* Entries of this type do <em>not</em> include the standard ZIP CRC-32 value.
|
||||
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
|
||||
*/
|
||||
const VV_AE_2 = 2;
|
||||
|
||||
const KEY_STRENGTH_128BIT = 128;
|
||||
const KEY_STRENGTH_192BIT = 192;
|
||||
const KEY_STRENGTH_256BIT = 256;
|
||||
|
||||
private static $keyStrengths = [
|
||||
self::KEY_STRENGTH_128BIT => 0x01,
|
||||
self::KEY_STRENGTH_192BIT => 0x02,
|
||||
self::KEY_STRENGTH_256BIT => 0x03
|
||||
];
|
||||
|
||||
/**
|
||||
* Vendor version.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $vendorVersion = self::VV_AE_1;
|
||||
|
||||
/**
|
||||
* Encryption strength.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $encryptionStrength = self::KEY_STRENGTH_256BIT;
|
||||
|
||||
/**
|
||||
* Zip compression method.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $method;
|
||||
|
||||
/**
|
||||
* Returns the Header ID (type) of this Extra Field.
|
||||
* The Header ID is an unsigned short integer (two bytes)
|
||||
* which must be constant during the life cycle of this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getHeaderId()
|
||||
{
|
||||
return 0x9901;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Data Size of this Extra Field.
|
||||
* The Data Size is an unsigned short integer (two bytes)
|
||||
* which indicates the length of the Data Block in bytes and does not
|
||||
* include its own size in this Extra Field.
|
||||
* This property may be initialized by calling ExtraField::readFrom.
|
||||
*
|
||||
* @return int The size of the Data Block in bytes
|
||||
* or 0 if unknown.
|
||||
*/
|
||||
public function getDataSize()
|
||||
{
|
||||
return self::DATA_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vendor version.
|
||||
*
|
||||
* @see WinZipAesEntryExtraField::VV_AE_1
|
||||
* @see WinZipAesEntryExtraField::VV_AE_2
|
||||
*/
|
||||
public function getVendorVersion()
|
||||
{
|
||||
return $this->vendorVersion & 0xffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vendor version.
|
||||
*
|
||||
* @see WinZipAesEntryExtraField::VV_AE_1
|
||||
* @see WinZipAesEntryExtraField::VV_AE_2
|
||||
* @param int $vendorVersion the vendor version.
|
||||
* @throws ZipException Unsupport vendor version.
|
||||
*/
|
||||
public function setVendorVersion($vendorVersion)
|
||||
{
|
||||
if ($vendorVersion < self::VV_AE_1 || self::VV_AE_2 < $vendorVersion) {
|
||||
throw new ZipException($vendorVersion);
|
||||
}
|
||||
$this->vendorVersion = $vendorVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns vendor id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getVendorId()
|
||||
{
|
||||
return self::VENDOR_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function getKeyStrength()
|
||||
{
|
||||
return self::keyStrength($this->encryptionStrength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $encryptionStrength Encryption strength as bits.
|
||||
* @return int
|
||||
* @throws ZipException If unsupport encryption strength.
|
||||
*/
|
||||
public static function keyStrength($encryptionStrength)
|
||||
{
|
||||
$flipKeyStrength = array_flip(self::$keyStrengths);
|
||||
if (!isset($flipKeyStrength[$encryptionStrength])) {
|
||||
throw new ZipException("Unsupport encryption strength " . $encryptionStrength);
|
||||
}
|
||||
return $flipKeyStrength[$encryptionStrength];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns compression method.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method & 0xffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets compression method.
|
||||
*
|
||||
* @param int $compressionMethod Compression method
|
||||
* @throws ZipException Compression method out of range.
|
||||
*/
|
||||
public function setMethod($compressionMethod)
|
||||
{
|
||||
if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) {
|
||||
throw new ZipException('Compression method out of range');
|
||||
}
|
||||
$this->method = $compressionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this Extra Field by deserializing a Data Block of
|
||||
* size bytes $size from the resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
* @param int $size Size
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function readFrom($handle, $off, $size)
|
||||
{
|
||||
if (self::DATA_SIZE != $size)
|
||||
throw new ZipException();
|
||||
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
/**
|
||||
* @var int $vendorVersion
|
||||
* @var int $vendorId
|
||||
* @var int $keyStrength
|
||||
* @var int $method
|
||||
*/
|
||||
$unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7));
|
||||
extract($unpack);
|
||||
$this->setVendorVersion($vendorVersion);
|
||||
if (self::VENDOR_ID != $vendorId) {
|
||||
throw new ZipException();
|
||||
}
|
||||
$this->setKeyStrength(self::keyStrength($keyStrength)); // checked
|
||||
$this->setMethod($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set key strength.
|
||||
*
|
||||
* @param int $keyStrength
|
||||
*/
|
||||
public function setKeyStrength($keyStrength)
|
||||
{
|
||||
$this->encryptionStrength = self::encryptionStrength($keyStrength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns encryption strength.
|
||||
*
|
||||
* @param int $keyStrength Key strength in bits.
|
||||
* @return int
|
||||
*/
|
||||
public static function encryptionStrength($keyStrength)
|
||||
{
|
||||
return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Data Block of ExtraField::getDataSize bytes to the
|
||||
* resource $handle at the zero based offset $off.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $off Offset bytes
|
||||
*/
|
||||
public function writeTo($handle, $off)
|
||||
{
|
||||
fseek($handle, $off, SEEK_SET);
|
||||
fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method));
|
||||
}
|
||||
}
|
42
src/PhpZip/Mapper/OffsetPositionMapper.php
Normal file
42
src/PhpZip/Mapper/OffsetPositionMapper.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace PhpZip\Mapper;
|
||||
|
||||
/**
|
||||
* Adds a offset value to the given position.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class OffsetPositionMapper extends PositionMapper
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $offset;
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
*/
|
||||
public function __construct($offset)
|
||||
{
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
* @return int
|
||||
*/
|
||||
public function map($position)
|
||||
{
|
||||
return parent::map($position) + $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
* @return int
|
||||
*/
|
||||
public function unmap($position)
|
||||
{
|
||||
return parent::unmap($position) - $this->offset;
|
||||
}
|
||||
}
|
29
src/PhpZip/Mapper/PositionMapper.php
Normal file
29
src/PhpZip/Mapper/PositionMapper.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace PhpZip\Mapper;
|
||||
|
||||
/**
|
||||
* Maps a given position.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class PositionMapper
|
||||
{
|
||||
/**
|
||||
* @param int $position
|
||||
* @return int
|
||||
*/
|
||||
public function map($position)
|
||||
{
|
||||
return $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
* @return int
|
||||
*/
|
||||
public function unmap($position)
|
||||
{
|
||||
return $position;
|
||||
}
|
||||
}
|
1187
src/PhpZip/Model/ZipEntry.php
Normal file
1187
src/PhpZip/Model/ZipEntry.php
Normal file
File diff suppressed because it is too large
Load Diff
506
src/PhpZip/Model/ZipInfo.php
Normal file
506
src/PhpZip/Model/ZipInfo.php
Normal file
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
namespace PhpZip\Model;
|
||||
|
||||
use PhpZip\Extra\NtfsExtraField;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Util\FilesUtil;
|
||||
|
||||
/**
|
||||
* Zip info
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipInfo
|
||||
{
|
||||
// made by constants
|
||||
const MADE_BY_MS_DOS = 0;
|
||||
const MADE_BY_AMIGA = 1;
|
||||
const MADE_BY_OPEN_VMS = 2;
|
||||
const MADE_BY_UNIX = 3;
|
||||
const MADE_BY_VM_CMS = 4;
|
||||
const MADE_BY_ATARI = 5;
|
||||
const MADE_BY_OS_2 = 6;
|
||||
const MADE_BY_MACINTOSH = 7;
|
||||
const MADE_BY_Z_SYSTEM = 8;
|
||||
const MADE_BY_CP_M = 9;
|
||||
const MADE_BY_WINDOWS_NTFS = 10;
|
||||
const MADE_BY_MVS = 11;
|
||||
const MADE_BY_VSE = 12;
|
||||
const MADE_BY_ACORN_RISC = 13;
|
||||
const MADE_BY_VFAT = 14;
|
||||
const MADE_BY_ALTERNATE_MVS = 15;
|
||||
const MADE_BY_BEOS = 16;
|
||||
const MADE_BY_TANDEM = 17;
|
||||
const MADE_BY_OS_400 = 18;
|
||||
const MADE_BY_OS_X = 19;
|
||||
const MADE_BY_UNKNOWN = 20;
|
||||
|
||||
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',
|
||||
self::MADE_BY_OPEN_VMS => 'OpenVMS',
|
||||
self::MADE_BY_UNIX => 'UNIX',
|
||||
self::MADE_BY_VM_CMS => 'VM/CMS',
|
||||
self::MADE_BY_ATARI => 'Atari ST',
|
||||
self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.',
|
||||
self::MADE_BY_MACINTOSH => 'Macintosh',
|
||||
self::MADE_BY_Z_SYSTEM => 'Z-System',
|
||||
self::MADE_BY_CP_M => 'CP/M',
|
||||
self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS',
|
||||
self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)',
|
||||
self::MADE_BY_VSE => 'VSE',
|
||||
self::MADE_BY_ACORN_RISC => 'Acorn Risc',
|
||||
self::MADE_BY_VFAT => 'VFAT',
|
||||
self::MADE_BY_ALTERNATE_MVS => 'Alternate MVS',
|
||||
self::MADE_BY_BEOS => 'BeOS',
|
||||
self::MADE_BY_TANDEM => 'Tandem',
|
||||
self::MADE_BY_OS_400 => 'OS/400',
|
||||
self::MADE_BY_OS_X => 'Mac OS X',
|
||||
];
|
||||
|
||||
private static $valuesCompressionMethod = [
|
||||
ZipEntry::METHOD_STORED => 'no compression',
|
||||
1 => 'shrink',
|
||||
2 => 'reduce level 1',
|
||||
3 => 'reduce level 2',
|
||||
4 => 'reduce level 3',
|
||||
5 => 'reduce level 4',
|
||||
6 => 'implode',
|
||||
7 => 'reserved for Tokenizing compression algorithm',
|
||||
ZipEntry::METHOD_DEFLATED => 'deflate',
|
||||
9 => 'deflate64',
|
||||
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
|
||||
11 => 'reserved by PKWARE',
|
||||
12 => 'bzip2',
|
||||
13 => 'reserved by PKWARE',
|
||||
14 => 'LZMA (EFS)',
|
||||
15 => 'reserved by PKWARE',
|
||||
16 => 'reserved by PKWARE',
|
||||
17 => 'reserved by PKWARE',
|
||||
18 => 'IBM TERSE',
|
||||
19 => 'IBM LZ77 z Architecture (PFS)',
|
||||
97 => 'WavPack',
|
||||
98 => 'PPMd version I, Rev 1',
|
||||
ZipEntry::WINZIP_AES => 'WinZip AES',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $folder;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $size;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $compressedSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $mtime;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $ctime;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $atime;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $encrypted;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $crc;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $method;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $platform;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $version;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $attributes;
|
||||
|
||||
/**
|
||||
* ZipInfo constructor.
|
||||
*
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$mtime = $entry->getTime();
|
||||
$atime = null;
|
||||
$ctime = null;
|
||||
|
||||
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
|
||||
if ($field !== null && $field instanceof NtfsExtraField) {
|
||||
/**
|
||||
* @var NtfsExtraField $field
|
||||
*/
|
||||
$atime = $field->getAtime();
|
||||
$ctime = $field->getCtime();
|
||||
}
|
||||
|
||||
$this->path = $entry->getName();
|
||||
$this->folder = $entry->isDirectory();
|
||||
$this->size = $entry->getSize();
|
||||
$this->compressedSize = $entry->getCompressedSize();
|
||||
$this->mtime = $mtime;
|
||||
$this->ctime = $ctime;
|
||||
$this->atime = $atime;
|
||||
$this->encrypted = $entry->isEncrypted();
|
||||
$this->comment = $entry->getComment();
|
||||
$this->crc = $entry->getCrc();
|
||||
$this->method = self::getMethodName($entry);
|
||||
$this->platform = self::getPlatformName($entry);
|
||||
$this->version = $entry->getVersionNeededToExtract();
|
||||
|
||||
$attribs = str_repeat(" ", 12);
|
||||
$xattr = (($entry->getRawExternalAttributes() >> 16) & 0xFFFF);
|
||||
switch ($entry->getPlatform()) {
|
||||
case self::MADE_BY_MS_DOS:
|
||||
case self::MADE_BY_WINDOWS_NTFS:
|
||||
if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
|
||||
($xattr & 0700) !=
|
||||
(0400 |
|
||||
(!($entry->getRawExternalAttributes() & 1) << 7) |
|
||||
(($entry->getRawExternalAttributes() & 0x10) << 2))
|
||||
) {
|
||||
$xattr = $entry->getRawExternalAttributes() & 0xFF;
|
||||
$attribs = ".r.-... ";
|
||||
$attribs[2] = ($xattr & 0x01) ? '-' : 'w';
|
||||
$attribs[5] = ($xattr & 0x02) ? 'h' : '-';
|
||||
$attribs[6] = ($xattr & 0x04) ? 's' : '-';
|
||||
$attribs[4] = ($xattr & 0x20) ? 'a' : '-';
|
||||
if ($xattr & 0x10) {
|
||||
$attribs[0] = 'd';
|
||||
$attribs[3] = 'x';
|
||||
} else
|
||||
$attribs[0] = '-';
|
||||
if ($xattr & 0x08)
|
||||
$attribs[0] = 'V';
|
||||
else {
|
||||
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
|
||||
$attribs[3] = 'x';
|
||||
}
|
||||
}
|
||||
break;
|
||||
} /* else: fall through! */
|
||||
|
||||
default: /* assume Unix-like */
|
||||
switch ($xattr & self::UNX_IFMT) {
|
||||
case self::UNX_IFDIR:
|
||||
$attribs[0] = 'd';
|
||||
break;
|
||||
case self::UNX_IFREG:
|
||||
$attribs[0] = '-';
|
||||
break;
|
||||
case self::UNX_IFLNK:
|
||||
$attribs[0] = 'l';
|
||||
break;
|
||||
case self::UNX_IFBLK:
|
||||
$attribs[0] = 'b';
|
||||
break;
|
||||
case self::UNX_IFCHR:
|
||||
$attribs[0] = 'c';
|
||||
break;
|
||||
case self::UNX_IFIFO:
|
||||
$attribs[0] = 'p';
|
||||
break;
|
||||
case self::UNX_IFSOCK:
|
||||
$attribs[0] = 's';
|
||||
break;
|
||||
default:
|
||||
$attribs[0] = '?';
|
||||
break;
|
||||
}
|
||||
$attribs[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
|
||||
$attribs[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
|
||||
$attribs[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
|
||||
$attribs[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
|
||||
$attribs[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
|
||||
$attribs[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
|
||||
|
||||
if ($xattr & self::UNX_IXUSR)
|
||||
$attribs[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
|
||||
else
|
||||
$attribs[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
|
||||
if ($xattr & self::UNX_IXGRP)
|
||||
$attribs[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
|
||||
else
|
||||
$attribs[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
|
||||
if ($xattr & self::UNX_IXOTH)
|
||||
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
|
||||
else
|
||||
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
|
||||
}
|
||||
$this->attributes = trim($attribs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
* @return string
|
||||
*/
|
||||
public static function getMethodName(ZipEntry $entry)
|
||||
{
|
||||
$return = '';
|
||||
if ($entry->isEncrypted()) {
|
||||
if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
|
||||
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
if ($field !== null) {
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$return .= '-' . $field->getKeyStrength();
|
||||
if (isset(self::$valuesCompressionMethod[$field->getMethod()])) {
|
||||
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$field->getMethod()]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$return .= 'ZipCrypto';
|
||||
if (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
|
||||
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
}
|
||||
}
|
||||
} elseif (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
|
||||
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
} else {
|
||||
$return = 'unknown';
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
* @return string
|
||||
*/
|
||||
public static function getPlatformName(ZipEntry $entry)
|
||||
{
|
||||
if (isset(self::$valuesMadeBy[$entry->getPlatform()])) {
|
||||
return self::$valuesMadeBy[$entry->getPlatform()];
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'path' => $this->getPath(),
|
||||
'folder' => $this->isFolder(),
|
||||
'size' => $this->getSize(),
|
||||
'compressed_size' => $this->getCompressedSize(),
|
||||
'modified' => $this->getMtime(),
|
||||
'created' => $this->getCtime(),
|
||||
'accessed' => $this->getAtime(),
|
||||
'attributes' => $this->getAttributes(),
|
||||
'encrypted' => $this->isEncrypted(),
|
||||
'comment' => $this->getComment(),
|
||||
'crc' => $this->getCrc(),
|
||||
'method' => $this->getMethod(),
|
||||
'platform' => $this->getPlatform(),
|
||||
'version' => $this->getVersion()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isFolder()
|
||||
{
|
||||
return $this->folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCompressedSize()
|
||||
{
|
||||
return $this->compressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMtime()
|
||||
{
|
||||
return $this->mtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCtime()
|
||||
{
|
||||
return $this->ctime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getAtime()
|
||||
{
|
||||
return $this->atime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEncrypted()
|
||||
{
|
||||
return $this->encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCrc()
|
||||
{
|
||||
return $this->crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPlatform()
|
||||
{
|
||||
return $this->platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
function __toString()
|
||||
{
|
||||
return 'ZipInfo {'
|
||||
. 'Path="' . $this->getPath() . '", '
|
||||
. ($this->isFolder() ? 'Folder, ' : '')
|
||||
. 'Size=' . FilesUtil::humanSize($this->getSize())
|
||||
. ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize())
|
||||
. ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', '
|
||||
. ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '')
|
||||
. ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '')
|
||||
. ($this->isEncrypted() ? 'Encrypted, ' : '')
|
||||
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
|
||||
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
|
||||
. 'Method="' . $this->getMethod() . '", '
|
||||
. 'Attributes="' . $this->getAttributes() . '", '
|
||||
. 'Platform="' . $this->getPlatform() . '", '
|
||||
. 'Version=' . $this->getVersion()
|
||||
. '}';
|
||||
}
|
||||
|
||||
|
||||
}
|
22
src/PhpZip/Output/ZipOutputEmptyDirEntry.php
Normal file
22
src/PhpZip/Output/ZipOutputEmptyDirEntry.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
/**
|
||||
* Zip output entry for empty dir.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputEmptyDirEntry extends ZipOutputEntry
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
46
src/PhpZip/Output/ZipOutputEntry.php
Normal file
46
src/PhpZip/Output/ZipOutputEntry.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Model\ZipEntry;
|
||||
|
||||
/**
|
||||
* Zip output Entry
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
if ($entry === null) {
|
||||
throw new \RuntimeException('entry is null');
|
||||
}
|
||||
$this->entry = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns zip entry
|
||||
*
|
||||
* @return ZipEntry
|
||||
*/
|
||||
public function getEntry()
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getEntryContent();
|
||||
}
|
54
src/PhpZip/Output/ZipOutputStreamEntry.php
Normal file
54
src/PhpZip/Output/ZipOutputStreamEntry.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Zip output entry for stream resource.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputStreamEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct($stream, ZipEntry $entry)
|
||||
{
|
||||
parent::__construct($entry);
|
||||
if (!is_resource($stream)) {
|
||||
throw new RuntimeException('stream is not resource');
|
||||
}
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
rewind($this->stream);
|
||||
return stream_get_contents($this->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release stream resource.
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
if ($this->stream !== null) {
|
||||
fclose($this->stream);
|
||||
$this->stream = null;
|
||||
}
|
||||
}
|
||||
}
|
46
src/PhpZip/Output/ZipOutputStringEntry.php
Normal file
46
src/PhpZip/Output/ZipOutputStringEntry.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
|
||||
/**
|
||||
* Zip output entry for string data.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputStringEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* Data content.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param ZipEntry $entry
|
||||
* @throws ZipException If data empty.
|
||||
*/
|
||||
public function __construct($data, ZipEntry $entry)
|
||||
{
|
||||
parent::__construct($entry);
|
||||
$data = (string)$data;
|
||||
if ($data === null) {
|
||||
throw new ZipException("data is null");
|
||||
}
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
56
src/PhpZip/Output/ZipOutputZipFileEntry.php
Normal file
56
src/PhpZip/Output/ZipOutputZipFileEntry.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace PhpZip\Output;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
/**
|
||||
* Zip output entry for input zip file.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipOutputZipFileEntry extends ZipOutputEntry
|
||||
{
|
||||
/**
|
||||
* Input zip file.
|
||||
*
|
||||
* @var ZipFile
|
||||
*/
|
||||
private $inputZipFile;
|
||||
|
||||
/**
|
||||
* Input entry name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $inputEntryName;
|
||||
|
||||
/**
|
||||
* ZipOutputZipFileEntry constructor.
|
||||
* @param ZipFile $zipFile
|
||||
* @param ZipEntry $zipEntry
|
||||
* @throws ZipException If input zip file is null.
|
||||
*/
|
||||
public function __construct(ZipFile $zipFile, ZipEntry $zipEntry)
|
||||
{
|
||||
if ($zipFile === null) {
|
||||
throw new ZipException('ZipFile is null');
|
||||
}
|
||||
parent::__construct(clone $zipEntry);
|
||||
|
||||
$this->inputZipFile = $zipFile;
|
||||
$this->inputEntryName = $zipEntry->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntryContent()
|
||||
{
|
||||
return $this->inputZipFile->getEntryContent($this->inputEntryName);
|
||||
}
|
||||
}
|
32
src/PhpZip/Util/CryptoUtil.php
Normal file
32
src/PhpZip/Util/CryptoUtil.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Crypto Utils
|
||||
*/
|
||||
class CryptoUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns random bytes.
|
||||
*
|
||||
* @param int $length
|
||||
* @return string
|
||||
* @throws ZipException
|
||||
*/
|
||||
public static final function randomBytes($length)
|
||||
{
|
||||
$length = (int)$length;
|
||||
if (function_exists('random_bytes')) {
|
||||
return random_bytes($length);
|
||||
} elseif (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return openssl_random_pseudo_bytes($length);
|
||||
} elseif (function_exists('mcrypt_create_iv')) {
|
||||
return mcrypt_create_iv($length);
|
||||
} else {
|
||||
throw new ZipException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
}
|
77
src/PhpZip/Util/DateTimeConverter.php
Normal file
77
src/PhpZip/Util/DateTimeConverter.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Convert unix timestamp values to DOS date/time values and vice versa.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class DateTimeConverter
|
||||
{
|
||||
/**
|
||||
* Smallest supported DOS date/time value in a ZIP file,
|
||||
* which is January 1st, 1980 AD 00:00:00 local time.
|
||||
*/
|
||||
const MIN_DOS_TIME = 0x210000; // (1 << 21) | (1 << 16)
|
||||
|
||||
/**
|
||||
* Largest supported DOS date/time value in a ZIP file,
|
||||
* which is December 31st, 2107 AD 23:59:58 local time.
|
||||
*/
|
||||
const MAX_DOS_TIME = 0xff9fbf7d; // ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 1);
|
||||
|
||||
/**
|
||||
* Convert a 32 bit integer DOS date/time value to a UNIX timestamp value.
|
||||
*
|
||||
* @param int $dosTime Dos date/time
|
||||
* @return int Unix timestamp
|
||||
*/
|
||||
public static function toUnixTimestamp($dosTime)
|
||||
{
|
||||
if (self::MIN_DOS_TIME > $dosTime) {
|
||||
$dosTime = self::MIN_DOS_TIME;
|
||||
} elseif (self::MAX_DOS_TIME < $dosTime) {
|
||||
$dosTime = self::MAX_DOS_TIME;
|
||||
}
|
||||
|
||||
return mktime(
|
||||
($dosTime >> 11) & 0x1f, // hour
|
||||
($dosTime >> 5) & 0x3f, // minute
|
||||
2 * ($dosTime & 0x1f), // second
|
||||
($dosTime >> 21) & 0x0f, // month
|
||||
($dosTime >> 16) & 0x1f, // day
|
||||
1980 + (($dosTime >> 25) & 0x7f) // year
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UNIX timestamp value to a DOS date/time value.
|
||||
*
|
||||
* @param int $unixTimestamp The number of seconds since midnight, January 1st,
|
||||
* 1970 AD UTC.
|
||||
* @return int A DOS date/time value reflecting the local time zone and
|
||||
* rounded down to even seconds
|
||||
* and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME.
|
||||
* @throws ZipException If unix timestamp is negative.
|
||||
*/
|
||||
public static function toDosTime($unixTimestamp)
|
||||
{
|
||||
if (0 > $unixTimestamp) {
|
||||
throw new ZipException("Negative unix timestamp: " . $unixTimestamp);
|
||||
}
|
||||
|
||||
$date = getdate($unixTimestamp);
|
||||
|
||||
if ($date['year'] < 1980) {
|
||||
return self::MIN_DOS_TIME;
|
||||
}
|
||||
|
||||
$date['year'] -= 1980;
|
||||
return ($date['year'] << 25 | $date['mon'] << 21 |
|
||||
$date['mday'] << 16 | $date['hours'] << 11 |
|
||||
$date['minutes'] << 5 | $date['seconds'] >> 1);
|
||||
}
|
||||
}
|
222
src/PhpZip/Util/FilesUtil.php
Normal file
222
src/PhpZip/Util/FilesUtil.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
|
||||
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
|
||||
|
||||
/**
|
||||
* Files util.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class FilesUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* Is empty directory
|
||||
*
|
||||
* @param string $dir Directory
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEmptyDir($dir)
|
||||
{
|
||||
if (!is_readable($dir)) {
|
||||
return false;
|
||||
}
|
||||
return count(scandir($dir)) === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove recursive directory.
|
||||
*
|
||||
* @param string $dir Directory path.
|
||||
*/
|
||||
public static function removeDir($dir)
|
||||
{
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($files as $fileInfo) {
|
||||
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
|
||||
$function($fileInfo->getRealPath());
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert glob pattern to regex pattern.
|
||||
*
|
||||
* @param string $globPattern
|
||||
* @return string
|
||||
*/
|
||||
public static function convertGlobToRegEx($globPattern)
|
||||
{
|
||||
// Remove beginning and ending * globs because they're useless
|
||||
$globPattern = trim($globPattern, '*');
|
||||
$escaping = false;
|
||||
$inCurrent = 0;
|
||||
$chars = str_split($globPattern);
|
||||
$regexPattern = '';
|
||||
foreach ($chars AS $currentChar) {
|
||||
switch ($currentChar) {
|
||||
case '*':
|
||||
$regexPattern .= ($escaping ? "\\*" : '.*');
|
||||
$escaping = false;
|
||||
break;
|
||||
case '?':
|
||||
$regexPattern .= ($escaping ? "\\?" : '.');
|
||||
$escaping = false;
|
||||
break;
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '+':
|
||||
case '|':
|
||||
case '^':
|
||||
case '$':
|
||||
case '@':
|
||||
case '%':
|
||||
$regexPattern .= '\\' . $currentChar;
|
||||
$escaping = false;
|
||||
break;
|
||||
case '\\':
|
||||
if ($escaping) {
|
||||
$regexPattern .= "\\\\";
|
||||
$escaping = false;
|
||||
} else {
|
||||
$escaping = true;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
if ($escaping) {
|
||||
$regexPattern .= "\\{";
|
||||
} else {
|
||||
$regexPattern = '(';
|
||||
$inCurrent++;
|
||||
}
|
||||
$escaping = false;
|
||||
break;
|
||||
case '}':
|
||||
if ($inCurrent > 0 && !$escaping) {
|
||||
$regexPattern .= ')';
|
||||
$inCurrent--;
|
||||
} else if ($escaping)
|
||||
$regexPattern = "\\}";
|
||||
else
|
||||
$regexPattern = "}";
|
||||
$escaping = false;
|
||||
break;
|
||||
case ',':
|
||||
if ($inCurrent > 0 && !$escaping) {
|
||||
$regexPattern .= '|';
|
||||
} else if ($escaping)
|
||||
$regexPattern .= "\\,";
|
||||
else
|
||||
$regexPattern = ",";
|
||||
break;
|
||||
default:
|
||||
$escaping = false;
|
||||
$regexPattern .= $currentChar;
|
||||
}
|
||||
}
|
||||
return $regexPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files.
|
||||
*
|
||||
* @param string $inputDir
|
||||
* @param bool $recursive
|
||||
* @param array $ignoreFiles
|
||||
* @return array Searched file list
|
||||
*/
|
||||
public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = [])
|
||||
{
|
||||
$directoryIterator = $recursive ?
|
||||
new \RecursiveDirectoryIterator($inputDir) :
|
||||
new \DirectoryIterator($inputDir);
|
||||
|
||||
if (!empty($ignoreFiles)) {
|
||||
$directoryIterator = $recursive ?
|
||||
new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles) :
|
||||
new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
|
||||
}
|
||||
|
||||
$iterator = $recursive ?
|
||||
new \RecursiveIteratorIterator($directoryIterator) :
|
||||
new \IteratorIterator($directoryIterator);
|
||||
|
||||
$fileList = [];
|
||||
foreach ($iterator as $file) {
|
||||
if ($file instanceof \SplFileInfo) {
|
||||
$fileList[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files from glob pattern.
|
||||
*
|
||||
* @param string $globPattern
|
||||
* @param int $flags
|
||||
* @param bool $recursive
|
||||
* @return array Searched file list
|
||||
*/
|
||||
public static function globFileSearch($globPattern, $flags = 0, $recursive = true)
|
||||
{
|
||||
$flags = (int)$flags;
|
||||
$recursive = (bool)$recursive;
|
||||
$files = glob($globPattern, $flags);
|
||||
if (!$recursive) {
|
||||
return $files;
|
||||
}
|
||||
foreach (glob(dirname($globPattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
|
||||
$files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive));
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files from regex pattern.
|
||||
*
|
||||
* @param string $folder
|
||||
* @param string $pattern
|
||||
* @param bool $recursive
|
||||
* @return array Searched file list
|
||||
*/
|
||||
public static function regexFileSearch($folder, $pattern, $recursive = true)
|
||||
{
|
||||
$directoryIterator = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder);
|
||||
$iterator = $recursive ? new \RecursiveIteratorIterator($directoryIterator) : new \IteratorIterator($directoryIterator);
|
||||
$regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
|
||||
$fileList = [];
|
||||
foreach ($regexIterator as $file) {
|
||||
if ($file instanceof \SplFileInfo) {
|
||||
$fileList[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to human size.
|
||||
*
|
||||
* @param int $size Size bytes
|
||||
* @param string|null $unit Unit support 'GB', 'MB', 'KB'
|
||||
* @return string
|
||||
*/
|
||||
public static function humanSize($size, $unit = null)
|
||||
{
|
||||
if (($unit === null && $size >= 1 << 30) || $unit === "GB")
|
||||
return number_format($size / (1 << 30), 2) . "GB";
|
||||
if (($unit === null && $size >= 1 << 20) || $unit === "MB")
|
||||
return number_format($size / (1 << 20), 2) . "MB";
|
||||
if (($unit === null && $size >= 1 << 10) || $unit === "KB")
|
||||
return number_format($size / (1 << 10), 2) . "KB";
|
||||
return number_format($size) . " bytes";
|
||||
}
|
||||
}
|
60
src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php
Normal file
60
src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace PhpZip\Util\Iterator;
|
||||
|
||||
use PhpZip\Util\StringUtil;
|
||||
|
||||
/**
|
||||
* Iterator for ignore files.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class IgnoreFilesFilterIterator extends \FilterIterator
|
||||
{
|
||||
/**
|
||||
* Ignore list files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $ignoreFiles = ['..'];
|
||||
|
||||
/**
|
||||
* @param \Iterator $iterator
|
||||
* @param array $ignoreFiles
|
||||
*/
|
||||
public function __construct(\Iterator $iterator, array $ignoreFiles)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
$this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current element of the iterator is acceptable
|
||||
* @link http://php.net/manual/en/filteriterator.accept.php
|
||||
* @return bool true if the current element is acceptable, otherwise false.
|
||||
* @since 5.1.0
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo $fileInfo
|
||||
*/
|
||||
$fileInfo = $this->current();
|
||||
$pathname = str_replace('\\', '/', $fileInfo->getPathname());
|
||||
foreach ($this->ignoreFiles as $ignoreFile) {
|
||||
// handler dir and sub dir
|
||||
if ($fileInfo->isDir()
|
||||
&& $ignoreFile[strlen($ignoreFile) - 1] === '/'
|
||||
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// handler filename
|
||||
if (StringUtil::endsWith($pathname, $ignoreFile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace PhpZip\Util\Iterator;
|
||||
|
||||
use PhpZip\Util\StringUtil;
|
||||
|
||||
/**
|
||||
* Recursive iterator for ignore files.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
|
||||
/**
|
||||
* Ignore list files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $ignoreFiles = ['..'];
|
||||
|
||||
/**
|
||||
* @param \RecursiveIterator $iterator
|
||||
* @param array $ignoreFiles
|
||||
*/
|
||||
public function __construct(\RecursiveIterator $iterator, array $ignoreFiles)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
$this->ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current element of the iterator is acceptable
|
||||
* @link http://php.net/manual/en/filteriterator.accept.php
|
||||
* @return bool true if the current element is acceptable, otherwise false.
|
||||
* @since 5.1.0
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo $fileInfo
|
||||
*/
|
||||
$fileInfo = $this->current();
|
||||
$pathname = str_replace('\\', '/', $fileInfo->getPathname());
|
||||
foreach ($this->ignoreFiles as $ignoreFile) {
|
||||
// handler dir and sub dir
|
||||
if ($fileInfo->isDir()
|
||||
&& $ignoreFile[strlen($ignoreFile) - 1] === '/'
|
||||
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// handler filename
|
||||
if (StringUtil::endsWith($pathname, $ignoreFile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IgnoreFilesRecursiveFilterIterator
|
||||
*/
|
||||
public function getChildren()
|
||||
{
|
||||
return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles);
|
||||
}
|
||||
}
|
48
src/PhpZip/Util/PackUtil.php
Normal file
48
src/PhpZip/Util/PackUtil.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Pack util
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class PackUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* @param int|string $longValue
|
||||
* @return string
|
||||
*/
|
||||
public static function packLongLE($longValue)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '5.6.3') >= 0) {
|
||||
return pack("P", $longValue);
|
||||
}
|
||||
|
||||
$left = 0xffffffff00000000;
|
||||
$right = 0x00000000ffffffff;
|
||||
|
||||
$r = ($longValue & $left) >> 32;
|
||||
$l = $longValue & $right;
|
||||
|
||||
return pack('VV', $l, $r);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $value
|
||||
* @return int
|
||||
* @throws ZipException
|
||||
*/
|
||||
public static function unpackLongLE($value)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '5.6.3') >= 0) {
|
||||
return current(unpack('P', $value));
|
||||
}
|
||||
$unpack = unpack('Va/Vb', $value);
|
||||
return $unpack['a'] + ($unpack['b'] << 32);
|
||||
}
|
||||
|
||||
}
|
30
src/PhpZip/Util/StringUtil.php
Normal file
30
src/PhpZip/Util/StringUtil.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
/**
|
||||
* String Util
|
||||
*/
|
||||
class StringUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
public static function startsWith($haystack, $needle)
|
||||
{
|
||||
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
public static function endsWith($haystack, $needle)
|
||||
{
|
||||
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
|
||||
&& strpos($haystack, $needle, $temp) !== false);
|
||||
}
|
||||
}
|
115
src/PhpZip/ZipConstants.php
Normal file
115
src/PhpZip/ZipConstants.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
/**
|
||||
* Constants for ZIP files.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
interface ZipConstants
|
||||
{
|
||||
/** Local File Header signature. */
|
||||
const LOCAL_FILE_HEADER_SIG = 0x04034B50;
|
||||
|
||||
/** Data Descriptor signature. */
|
||||
const DATA_DESCRIPTOR_SIG = 0x08074B50;
|
||||
|
||||
/** Central File Header signature. */
|
||||
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
|
||||
|
||||
/** Zip64 End Of Central Directory Record. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
|
||||
|
||||
/** Zip64 End Of Central Directory Locator. */
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
|
||||
|
||||
/** End Of Central Directory Record signature. */
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
|
||||
|
||||
/**
|
||||
* The minimum length of the Local File Header record.
|
||||
*
|
||||
* local file header signature 4
|
||||
* version needed to extract 2
|
||||
* general purpose bit flag 2
|
||||
* compression method 2
|
||||
* last mod file time 2
|
||||
* last mod file date 2
|
||||
* crc-32 4
|
||||
* compressed size 4
|
||||
* uncompressed size 4
|
||||
* file name length 2
|
||||
* extra field length 2
|
||||
*/
|
||||
const LOCAL_FILE_HEADER_MIN_LEN = 30;
|
||||
|
||||
/**
|
||||
* The minimum length of the End Of Central Directory Record.
|
||||
*
|
||||
* end of central dir signature 4
|
||||
* number of this disk 2
|
||||
* number of the disk with the
|
||||
* start of the central directory 2
|
||||
* total number of entries in the
|
||||
* central directory on this disk 2
|
||||
* total number of entries in
|
||||
* the central directory 2
|
||||
* size of the central directory 4
|
||||
* offset of start of central *
|
||||
* directory with respect to *
|
||||
* the starting disk number 4
|
||||
* zipfile comment length 2
|
||||
*/
|
||||
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
|
||||
|
||||
/**
|
||||
* The length of the Zip64 End Of Central Directory Locator.
|
||||
* zip64 end of central dir locator
|
||||
* signature 4
|
||||
* number of the disk with the
|
||||
* start of the zip64 end of
|
||||
* central directory 4
|
||||
* relative offset of the zip64
|
||||
* end of central directory record 8
|
||||
* total number of disks 4
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
|
||||
|
||||
/**
|
||||
* The minimum length of the Zip64 End Of Central Directory Record.
|
||||
*
|
||||
* zip64 end of central dir
|
||||
* signature 4
|
||||
* size of zip64 end of central
|
||||
* directory record 8
|
||||
* version made by 2
|
||||
* version needed to extract 2
|
||||
* number of this disk 4
|
||||
* number of the disk with the
|
||||
* start of the central directory 4
|
||||
* total number of entries in the
|
||||
* central directory on this disk 8
|
||||
* total number of entries in
|
||||
* the central directory 8
|
||||
* size of the central directory 8
|
||||
* offset of start of central
|
||||
* directory with respect to
|
||||
* the starting disk number 8
|
||||
*/
|
||||
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
|
||||
|
||||
/**
|
||||
* Local File Header signature 4
|
||||
* Version Needed To Extract 2
|
||||
* General Purpose Bit Flags 2
|
||||
* Compression Method 2
|
||||
* Last Mod File Time 2
|
||||
* Last Mod File Date 2
|
||||
* CRC-32 4
|
||||
* Compressed Size 4
|
||||
* Uncompressed Size 4
|
||||
*/
|
||||
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
|
||||
|
||||
}
|
915
src/PhpZip/ZipFile.php
Normal file
915
src/PhpZip/ZipFile.php
Normal file
@@ -0,0 +1,915 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
|
||||
use PhpZip\Crypto\WinZipAesEngine;
|
||||
use PhpZip\Exception\Crc32Exception;
|
||||
use PhpZip\Exception\IllegalArgumentException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Exception\ZipNotFoundEntry;
|
||||
use PhpZip\Exception\ZipUnsupportMethod;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Mapper\OffsetPositionMapper;
|
||||
use PhpZip\Mapper\PositionMapper;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Model\ZipInfo;
|
||||
use PhpZip\Util\PackUtil;
|
||||
|
||||
/**
|
||||
* This class is able to open the .ZIP file in read mode and extract files from it.
|
||||
*
|
||||
* Implemented support traditional PKWARE encryption and WinZip AES encryption.
|
||||
* Implemented support ZIP64.
|
||||
* Implemented support skip a preamble like the one found in self extracting archives.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
|
||||
{
|
||||
/**
|
||||
* Input seekable stream resource.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $inputStream;
|
||||
|
||||
/**
|
||||
* The total number of bytes in the ZIP archive.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $length;
|
||||
|
||||
/**
|
||||
* The charset to use for entry names and comments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $charset;
|
||||
|
||||
/**
|
||||
* The number of bytes in the preamble of this ZIP file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $preamble;
|
||||
|
||||
/**
|
||||
* The number of bytes in the postamble of this ZIP file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $postamble;
|
||||
|
||||
/**
|
||||
* Maps entry names to zip entries.
|
||||
*
|
||||
* @var ZipEntry[]
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* The file comment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* Maps offsets specified in the ZIP file to real offsets in the file.
|
||||
*
|
||||
* @var PositionMapper
|
||||
*/
|
||||
private $mapper;
|
||||
|
||||
/**
|
||||
* Private ZipFile constructor.
|
||||
*
|
||||
* @see ZipFile::openFromFile()
|
||||
* @see ZipFile::openFromString()
|
||||
* @see ZipFile::openFromStream()
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->mapper = new PositionMapper();
|
||||
$this->charset = "UTF-8";
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from file
|
||||
*
|
||||
* @param string $filename
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException if file doesn't exists.
|
||||
* @throws ZipException if can't open file.
|
||||
*/
|
||||
public static function openFromFile($filename)
|
||||
{
|
||||
if (!file_exists($filename)) {
|
||||
throw new IllegalArgumentException("File $filename can't exists.");
|
||||
}
|
||||
if (!($handle = fopen($filename, 'rb'))) {
|
||||
throw new ZipException("File $filename can't open.");
|
||||
}
|
||||
$zipFile = self::openFromStream($handle);
|
||||
$zipFile->length = filesize($filename);
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from stream resource
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException Invalid stream resource
|
||||
* or resource cannot seekable stream
|
||||
*/
|
||||
public static function openFromStream($handle)
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
throw new IllegalArgumentException("Invalid stream resource.");
|
||||
}
|
||||
$meta = stream_get_meta_data($handle);
|
||||
if (!$meta['seekable']) {
|
||||
throw new IllegalArgumentException("Resource cannot seekable stream.");
|
||||
}
|
||||
$zipFile = new self();
|
||||
$stats = fstat($handle);
|
||||
if (isset($stats['size'])) {
|
||||
$zipFile->length = $stats['size'];
|
||||
}
|
||||
$zipFile->checkZipFileSignature($handle);
|
||||
$numEntries = $zipFile->findCentralDirectory($handle);
|
||||
$zipFile->mountCentralDirectory($handle, $numEntries);
|
||||
if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) {
|
||||
assert(0 === $numEntries);
|
||||
$zipFile->checkZipFileSignature($handle);
|
||||
}
|
||||
assert(null !== $handle);
|
||||
assert(null !== $zipFile->charset);
|
||||
assert(null !== $zipFile->entries);
|
||||
assert(null !== $zipFile->mapper);
|
||||
$zipFile->inputStream = $handle;
|
||||
// Do NOT close stream!
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZipOutputFile
|
||||
*/
|
||||
public function edit(){
|
||||
return ZipOutputFile::openFromZipFile($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check zip file signature
|
||||
*
|
||||
* @param resource $handle
|
||||
* @throws ZipException if this not .ZIP file.
|
||||
*/
|
||||
private function checkZipFileSignature($handle)
|
||||
{
|
||||
rewind($handle);
|
||||
$signature = current(unpack('V', fread($handle, 4)));
|
||||
// Constraint: A ZIP file must start with a Local File Header
|
||||
// or a (ZIP64) End Of Central Directory Record if it's empty.
|
||||
if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
|
||||
) {
|
||||
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the file pointer at the first Central File Header.
|
||||
* Performs some means to check that this is really a ZIP file.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return int
|
||||
* @throws ZipException If the file is not compatible to the ZIP File
|
||||
* Format Specification.
|
||||
*/
|
||||
private function findCentralDirectory($handle)
|
||||
{
|
||||
// Search for End of central directory record.
|
||||
$max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
|
||||
$min = $max >= 0xffff ? $max - 0xffff : 0;
|
||||
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
|
||||
fseek($handle, $endOfCentralDirRecordPos, SEEK_SET);
|
||||
// end of central dir signature 4 bytes (0x06054b50)
|
||||
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4))))
|
||||
continue;
|
||||
|
||||
// Process End Of Central Directory Record.
|
||||
$data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4);
|
||||
|
||||
/**
|
||||
* @var int $diskNo number of this disk - 2 bytes
|
||||
* @var int $cdDiskNo number of the disk with the start of the
|
||||
* central directory - 2 bytes
|
||||
* @var int $cdEntriesDisk total number of entries in the central
|
||||
* directory on this disk - 2 bytes
|
||||
* @var int $cdEntries total number of entries in the central
|
||||
* directory - 2 bytes
|
||||
* @var int $cdSize size of the central directory - 4 bytes
|
||||
* @var int $cdPos offset of start of central directory with
|
||||
* respect to the starting disk number - 4 bytes
|
||||
* @var int $commentLen ZIP file comment length - 2 bytes
|
||||
*/
|
||||
$unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data);
|
||||
extract($unpack);
|
||||
|
||||
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
|
||||
throw new ZipException(
|
||||
"ZIP file spanning/splitting is not supported!"
|
||||
);
|
||||
}
|
||||
// .ZIP file comment (variable size)
|
||||
if (0 < $commentLen) {
|
||||
$this->comment = fread($handle, $commentLen);
|
||||
}
|
||||
$this->preamble = $endOfCentralDirRecordPos;
|
||||
$this->postamble = $this->length - ftell($handle);
|
||||
|
||||
// Check for ZIP64 End Of Central Directory Locator.
|
||||
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
|
||||
|
||||
fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET);
|
||||
|
||||
// zip64 end of central dir locator
|
||||
// signature 4 bytes (0x07064b50)
|
||||
if (
|
||||
0 > $endOfCentralDirLocatorPos ||
|
||||
ftell($handle) === $this->length ||
|
||||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4)))
|
||||
) {
|
||||
// Seek and check first CFH, probably requiring an offset mapper.
|
||||
$offset = $endOfCentralDirRecordPos - $cdSize;
|
||||
fseek($handle, $offset, SEEK_SET);
|
||||
$offset -= $cdPos;
|
||||
if (0 !== $offset) {
|
||||
$this->mapper = new OffsetPositionMapper($offset);
|
||||
}
|
||||
return (int)$cdEntries;
|
||||
}
|
||||
|
||||
// number of the disk with the
|
||||
// start of the zip64 end of
|
||||
// central directory 4 bytes
|
||||
$zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4)));
|
||||
// relative offset of the zip64
|
||||
// end of central directory record 8 bytes
|
||||
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// total number of disks 4 bytes
|
||||
$totalDisks = current(unpack('V', fread($handle, 4)));
|
||||
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
|
||||
throw new ZipException("ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
|
||||
// zip64 end of central dir
|
||||
// signature 4 bytes (0x06064b50)
|
||||
$zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4)));
|
||||
if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
|
||||
throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
|
||||
}
|
||||
// size of zip64 end of central
|
||||
// directory record 8 bytes
|
||||
// version made by 2 bytes
|
||||
// version needed to extract 2 bytes
|
||||
fseek($handle, 12, SEEK_CUR);
|
||||
// number of this disk 4 bytes
|
||||
$diskNo = current(unpack('V', fread($handle, 4)));
|
||||
// number of the disk with the
|
||||
// start of the central directory 4 bytes
|
||||
$cdDiskNo = current(unpack('V', fread($handle, 4)));
|
||||
// total number of entries in the
|
||||
// central directory on this disk 8 bytes
|
||||
$cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// total number of entries in the
|
||||
// central directory 8 bytes
|
||||
$cdEntries = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
|
||||
throw new ZipException(
|
||||
"ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
|
||||
throw new ZipException(
|
||||
"Total Number Of Entries In The Central Directory out of range!");
|
||||
}
|
||||
// size of the central directory 8 bytes
|
||||
//$cdSize = self::getLongLE($channel);
|
||||
fseek($handle, 8, SEEK_CUR);
|
||||
// offset of start of central
|
||||
// directory with respect to
|
||||
// the starting disk number 8 bytes
|
||||
$cdPos = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// zip64 extensible data sector (variable size)
|
||||
fseek($handle, $cdPos, SEEK_SET);
|
||||
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
|
||||
return (int)$cdEntries;
|
||||
}
|
||||
// Start recovering file entries from min.
|
||||
$this->preamble = $min;
|
||||
$this->postamble = $this->length - $min;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the central directory from the given seekable byte channel
|
||||
* and populates the internal tables with ZipEntry instances.
|
||||
*
|
||||
* The ZipEntry's will know all data that can be obtained from the
|
||||
* central directory alone, but not the data that requires the local
|
||||
* file header or additional data to be read.
|
||||
*
|
||||
* @param resource $handle Input channel.
|
||||
* @param int $numEntries Size zip entries.
|
||||
* @throws ZipException
|
||||
*/
|
||||
private function mountCentralDirectory($handle, $numEntries)
|
||||
{
|
||||
$numEntries = (int)$numEntries;
|
||||
$entries = [];
|
||||
for (; ; $numEntries--) {
|
||||
// central file header signature 4 bytes (0x02014b50)
|
||||
if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) {
|
||||
break;
|
||||
}
|
||||
// version made by 2 bytes
|
||||
$versionMadeBy = current(unpack('v', fread($handle, 2)));
|
||||
|
||||
// version needed to extract 2 bytes
|
||||
fseek($handle, 2, SEEK_CUR);
|
||||
|
||||
$unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26));
|
||||
|
||||
// disk number start 2 bytes
|
||||
// internal file attributes 2 bytes
|
||||
fseek($handle, 4, SEEK_CUR);
|
||||
|
||||
// external file attributes 4 bytes
|
||||
// relative offset of local header 4 bytes
|
||||
$unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8));
|
||||
|
||||
$utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8);
|
||||
if ($utf8) {
|
||||
$this->charset = "UTF-8";
|
||||
}
|
||||
|
||||
// See appendix D of PKWARE's ZIP File Format Specification.
|
||||
$name = fread($handle, $unpack['fileLen']);
|
||||
$entry = new ZipEntry($name, $handle);
|
||||
$entry->setRawPlatform($versionMadeBy >> 8);
|
||||
$entry->setGeneralPurposeBitFlags($unpack['gpbf']);
|
||||
$entry->setRawMethod($unpack['rawMethod']);
|
||||
$entry->setRawTime($unpack['rawTime']);
|
||||
$entry->setRawCrc($unpack['rawCrc']);
|
||||
$entry->setRawCompressedSize($unpack['rawCompressedSize']);
|
||||
$entry->setRawSize($unpack['rawSize']);
|
||||
$entry->setRawExternalAttributes($unpack2['rawExternalAttributes']);
|
||||
$entry->setRawOffset($unpack2['lfhOff']); // must be unmapped!
|
||||
if (0 < $unpack['extraLen']) {
|
||||
$entry->setRawExtraFields(fread($handle, $unpack['extraLen']));
|
||||
}
|
||||
if (0 < $unpack['commentLen']) {
|
||||
$entry->setComment(fread($handle, $unpack['commentLen']));
|
||||
}
|
||||
|
||||
unset($unpack, $unpack2);
|
||||
|
||||
// Re-load virtual offset after ZIP64 Extended Information
|
||||
// Extra Field may have been parsed, map it to the real
|
||||
// offset and conditionally update the preamble size from it.
|
||||
$lfhOff = $this->mapper->map($entry->getOffset());
|
||||
if ($lfhOff < $this->preamble) {
|
||||
$this->preamble = $lfhOff;
|
||||
}
|
||||
$entries[$entry->getName()] = $entry;
|
||||
}
|
||||
|
||||
if (0 !== $numEntries % 0x10000) {
|
||||
throw new ZipException("Expected " . abs($numEntries) .
|
||||
($numEntries > 0 ? " more" : " less") .
|
||||
" entries in the Central Directory!");
|
||||
}
|
||||
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from raw string data.
|
||||
*
|
||||
* @param string $data
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException if data not available.
|
||||
* @throws ZipException if can't open temp stream.
|
||||
*/
|
||||
public static function openFromString($data)
|
||||
{
|
||||
if (null === $data || strlen($data) === 0) {
|
||||
throw new IllegalArgumentException("Data not available");
|
||||
}
|
||||
if (!($handle = fopen('php://temp', 'r+b'))) {
|
||||
throw new ZipException("Can't open temp stream.");
|
||||
}
|
||||
fwrite($handle, $data);
|
||||
rewind($handle);
|
||||
$zipFile = self::openFromStream($handle);
|
||||
$zipFile->length = strlen($data);
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in this ZIP file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return sizeof($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list files.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getListFiles()
|
||||
{
|
||||
return array_keys($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return ZipEntry[]
|
||||
*/
|
||||
public function getRawEntries()
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a entry exists
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasEntry($entryName)
|
||||
{
|
||||
return isset($this->entries[$entryName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the directory entry.
|
||||
* Returns true if and only if this ZIP entry represents a directory entry
|
||||
* (i.e. end with '/').
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return bool
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function isDirectory($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
return $this->entries[$entryName]->isDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password to all encrypted entries.
|
||||
*
|
||||
* @param string $password Password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
foreach ($this->entries as $entry) {
|
||||
if ($entry->isEncrypted()) {
|
||||
$entry->setPassword($password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password to concrete zip entry.
|
||||
*
|
||||
* @param string $entryName Zip entry name
|
||||
* @param string $password Password
|
||||
* @throws ZipNotFoundEntry if don't exist zip entry.
|
||||
*/
|
||||
public function setEntryPassword($entryName, $password)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
if ($entry->isEncrypted()) {
|
||||
$entry->setPassword($password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file comment.
|
||||
*
|
||||
* @return string The file comment.
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return null === $this->comment ? '' : $this->decode($this->comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode charset entry name.
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function decode($text)
|
||||
{
|
||||
$inCharset = mb_detect_encoding($text, mb_detect_order(), true);
|
||||
if ($inCharset === $this->charset) return $text;
|
||||
return iconv($inCharset, $this->charset, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry comment.
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return string
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function getEntryComment($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry("Not found entry " . $entryName);
|
||||
}
|
||||
return $this->entries[$entryName]->getComment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the character set which is effectively used for
|
||||
* decoding entry names and the file comment.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file length of this ZIP file in bytes.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function length()
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info by entry.
|
||||
*
|
||||
* @param string|ZipEntry $entryName
|
||||
* @return ZipInfo
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function getEntryInfo($entryName)
|
||||
{
|
||||
if ($entryName instanceof ZipEntry) {
|
||||
$entryName = $entryName->getName();
|
||||
}
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
|
||||
return new ZipInfo($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info by all entries.
|
||||
*
|
||||
* @return ZipInfo[]
|
||||
*/
|
||||
public function getAllInfo()
|
||||
{
|
||||
return array_map([$this, 'getEntryInfo'], $this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the archive contents
|
||||
*
|
||||
* Extract the complete archive or the given files to the specified destination.
|
||||
*
|
||||
* @param string $destination Location where to extract the files.
|
||||
* @param array $entries The entries to extract. It accepts
|
||||
* either a single entry name or an array of names.
|
||||
* @return bool
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function extractTo($destination, $entries = null)
|
||||
{
|
||||
if ($this->entries === null) {
|
||||
throw new ZipException("Zip entries not initial");
|
||||
}
|
||||
if (!file_exists($destination)) {
|
||||
throw new ZipException("Destination " . $destination . " not found");
|
||||
}
|
||||
if (!is_dir($destination)) {
|
||||
throw new ZipException("Destination is not directory");
|
||||
}
|
||||
if (!is_writable($destination)) {
|
||||
throw new ZipException("Destination is not writable directory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @var ZipEntry[] $zipEntries
|
||||
*/
|
||||
if (!empty($entries)) {
|
||||
if (is_string($entries)) {
|
||||
$entries = (array)$entries;
|
||||
}
|
||||
if (is_array($entries)) {
|
||||
$flipEntries = array_flip($entries);
|
||||
$zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) {
|
||||
/**
|
||||
* @var ZipEntry $zipEntry
|
||||
*/
|
||||
return isset($flipEntries[$zipEntry->getName()]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$zipEntries = $this->entries;
|
||||
}
|
||||
|
||||
$extract = 0;
|
||||
foreach ($zipEntries AS $entry) {
|
||||
$file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
|
||||
if ($entry->isDirectory()) {
|
||||
if (!is_dir($file)) {
|
||||
if (!mkdir($file, 0755, true)) {
|
||||
throw new ZipException("Can not create dir " . $file);
|
||||
}
|
||||
chmod($file, 0755);
|
||||
touch($file, $entry->getTime());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$dir = dirname($file);
|
||||
if (!file_exists($dir)) {
|
||||
if (!mkdir($dir, 0755, true)) {
|
||||
throw new ZipException("Can not create dir " . $dir);
|
||||
}
|
||||
chmod($dir, 0755);
|
||||
touch($file, $entry->getTime());
|
||||
}
|
||||
if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) {
|
||||
return false;
|
||||
}
|
||||
touch($file, $entry->getTime());
|
||||
$extract++;
|
||||
}
|
||||
return $extract > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return string|null
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
|
||||
$pos = $entry->getOffset();
|
||||
assert(ZipEntry::UNKNOWN !== $pos);
|
||||
$startPos = $pos = $this->mapper->map($pos);
|
||||
fseek($this->inputStream, $pos, SEEK_SET);
|
||||
$localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4)));
|
||||
if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) {
|
||||
throw new ZipException($entry->getName() . " (expected Local File Header)");
|
||||
}
|
||||
fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET);
|
||||
$unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4));
|
||||
$pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen'];
|
||||
|
||||
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
|
||||
|
||||
$check = $entry->isEncrypted();
|
||||
$method = $entry->getMethod();
|
||||
|
||||
$password = $entry->getPassword();
|
||||
if ($entry->isEncrypted() && empty($password)) {
|
||||
throw new ZipException("Not set password");
|
||||
}
|
||||
// Strong Encryption Specification - WinZip AES
|
||||
if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) {
|
||||
fseek($this->inputStream, $pos, SEEK_SET);
|
||||
$winZipAesEngine = new WinZipAesEngine($entry);
|
||||
$content = $winZipAesEngine->decrypt($this->inputStream);
|
||||
// Disable redundant CRC-32 check.
|
||||
$check = false;
|
||||
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$method = $field->getMethod();
|
||||
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES);
|
||||
} else {
|
||||
// Get raw entry content
|
||||
$content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos);
|
||||
|
||||
// Traditional PKWARE Decryption
|
||||
if ($entry->isEncrypted()) {
|
||||
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
|
||||
$content = $zipCryptoEngine->decrypt($content);
|
||||
|
||||
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
|
||||
}
|
||||
}
|
||||
if ($check) {
|
||||
// Check CRC32 in the Local File Header or Data Descriptor.
|
||||
$localCrc = null;
|
||||
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
// The CRC32 is in the Data Descriptor after the compressed
|
||||
// size.
|
||||
// Note the Data Descriptor's Signature is optional:
|
||||
// All newer apps should write it (and so does TrueVFS),
|
||||
// but older apps might not.
|
||||
fseek($this->inputStream, $pos + $entry->getCompressedSize(), SEEK_SET);
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
}
|
||||
} else {
|
||||
fseek($this->inputStream, $startPos + 14, SEEK_SET);
|
||||
// The CRC32 in the Local File Header.
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
}
|
||||
if ($entry->getCrc() !== $localCrc) {
|
||||
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case ZipEntry::METHOD_STORED:
|
||||
break;
|
||||
case ZipEntry::METHOD_DEFLATED:
|
||||
$content = gzinflate($content);
|
||||
break;
|
||||
case ZipEntry::METHOD_BZIP2:
|
||||
if (!extension_loaded('bz2')) {
|
||||
throw new ZipException('Extension bzip2 not install');
|
||||
}
|
||||
$content = bzdecompress($content);
|
||||
break;
|
||||
default:
|
||||
throw new ZipUnsupportMethod($entry->getName()
|
||||
. " (compression method "
|
||||
. $method
|
||||
. " is not supported)");
|
||||
}
|
||||
if ($check) {
|
||||
$localCrc = crc32($content);
|
||||
if ($entry->getCrc() !== $localCrc) {
|
||||
if ($entry->isEncrypted()) {
|
||||
throw new ZipCryptoException("Wrong password");
|
||||
}
|
||||
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all resources
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close zip archive and release input stream.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->length = null;
|
||||
|
||||
if ($this->inputStream !== null) {
|
||||
fclose($this->inputStream);
|
||||
$this->inputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a offset exists
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
* @param string $entryName An offset to check for.
|
||||
* @return boolean true on success or false on failure.
|
||||
* The return value will be casted to boolean if non-boolean was returned.
|
||||
*/
|
||||
public function offsetExists($entryName)
|
||||
{
|
||||
return isset($this->entries[$entryName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||
* @param string $entryName The offset to retrieve.
|
||||
* @return string|null
|
||||
*/
|
||||
public function offsetGet($entryName)
|
||||
{
|
||||
return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||
* @param string $entryName The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @throws ZipUnsupportMethod
|
||||
*/
|
||||
public function offsetSet($entryName, $value)
|
||||
{
|
||||
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
* @param string $entryName The offset to unset.
|
||||
* @throws ZipUnsupportMethod
|
||||
*/
|
||||
public function offsetUnset($entryName)
|
||||
{
|
||||
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @return mixed Can return any type.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->offsetGet($this->key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
* @return void Any returned value is ignored.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
next($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return key($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @return boolean The return value will be casted to boolean and then evaluated.
|
||||
* Returns true on success or false on failure.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->offsetExists($this->key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
* @return void Any returned value is ignored.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->entries);
|
||||
}
|
||||
}
|
1475
src/PhpZip/ZipOutputFile.php
Normal file
1475
src/PhpZip/ZipOutputFile.php
Normal file
File diff suppressed because it is too large
Load Diff
905
src/ZipEntry.php
905
src/ZipEntry.php
@@ -1,905 +0,0 @@
|
||||
<?php
|
||||
namespace Nelexa\Zip;
|
||||
|
||||
use Nelexa\Buffer\Buffer;
|
||||
use Nelexa\Buffer\StringBuffer;
|
||||
|
||||
class ZipEntry
|
||||
{
|
||||
// made by constants
|
||||
const MADE_BY_MS_DOS = 0;
|
||||
const MADE_BY_AMIGA = 1;
|
||||
const MADE_BY_OPEN_VMS = 2;
|
||||
const MADE_BY_UNIX = 3;
|
||||
const MADE_BY_VM_CMS = 4;
|
||||
const MADE_BY_ATARI = 5;
|
||||
const MADE_BY_OS_2 = 6;
|
||||
const MADE_BY_MACINTOSH = 7;
|
||||
const MADE_BY_Z_SYSTEM = 8;
|
||||
const MADE_BY_CP_M = 9;
|
||||
const MADE_BY_WINDOWS_NTFS = 10;
|
||||
const MADE_BY_MVS = 11;
|
||||
const MADE_BY_VSE = 12;
|
||||
const MADE_BY_ACORN_RISC = 13;
|
||||
const MADE_BY_VFAT = 14;
|
||||
const MADE_BY_ALTERNATE_MVS = 15;
|
||||
const MADE_BY_BEOS = 16;
|
||||
const MADE_BY_TANDEM = 17;
|
||||
const MADE_BY_OS_400 = 18;
|
||||
const MADE_BY_OS_X = 19;
|
||||
const MADE_BY_UNKNOWN = 20;
|
||||
|
||||
private static $valuesMadeBy = array(
|
||||
self::MADE_BY_MS_DOS => 'MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)',
|
||||
self::MADE_BY_AMIGA => 'Amiga',
|
||||
self::MADE_BY_OPEN_VMS => 'OpenVMS',
|
||||
self::MADE_BY_UNIX => 'UNIX',
|
||||
self::MADE_BY_VM_CMS => 'VM/CMS',
|
||||
self::MADE_BY_ATARI => 'Atari ST',
|
||||
self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.',
|
||||
self::MADE_BY_MACINTOSH => 'Macintosh',
|
||||
self::MADE_BY_Z_SYSTEM => 'Z-System',
|
||||
self::MADE_BY_CP_M => 'CP/M',
|
||||
self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS',
|
||||
self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)',
|
||||
self::MADE_BY_VSE => 'VSE',
|
||||
self::MADE_BY_ACORN_RISC => 'Acorn Risc',
|
||||
self::MADE_BY_VFAT => 'VFAT',
|
||||
self::MADE_BY_ALTERNATE_MVS => 'alternate MVS',
|
||||
self::MADE_BY_BEOS => 'BeOS',
|
||||
self::MADE_BY_TANDEM => 'Tandem',
|
||||
self::MADE_BY_OS_400 => 'OS/400',
|
||||
self::MADE_BY_OS_X => 'OS X (Darwin)',
|
||||
);
|
||||
|
||||
// constants version by extract
|
||||
const EXTRACT_VERSION_10 = 10;
|
||||
const EXTRACT_VERSION_11 = 11;
|
||||
const EXTRACT_VERSION_20 = 20;
|
||||
//1.0 - Default value
|
||||
//1.1 - File is a volume label
|
||||
//2.0 - File is a folder (directory)
|
||||
//2.0 - File is compressed using Deflate compression
|
||||
//2.0 - File is encrypted using traditional PKWARE encryption
|
||||
//2.1 - File is compressed using Deflate64(tm)
|
||||
//2.5 - File is compressed using PKWARE DCL Implode
|
||||
//2.7 - File is a patch data set
|
||||
//4.5 - File uses ZIP64 format extensions
|
||||
//4.6 - File is compressed using BZIP2 compression*
|
||||
//5.0 - File is encrypted using DES
|
||||
//5.0 - File is encrypted using 3DES
|
||||
//5.0 - File is encrypted using original RC2 encryption
|
||||
//5.0 - File is encrypted using RC4 encryption
|
||||
//5.1 - File is encrypted using AES encryption
|
||||
//5.1 - File is encrypted using corrected RC2 encryption**
|
||||
//5.2 - File is encrypted using corrected RC2-64 encryption**
|
||||
//6.1 - File is encrypted using non-OAEP key wrapping***
|
||||
//6.2 - Central directory encryption
|
||||
//6.3 - File is compressed using LZMA
|
||||
//6.3 - File is compressed using PPMd+
|
||||
//6.3 - File is encrypted using Blowfish
|
||||
//6.3 - File is encrypted using Twofish
|
||||
|
||||
const FLAG_ENCRYPTION = 0;
|
||||
const FLAG_DATA_DESCRIPTION = 3;
|
||||
const FLAG_UTF8 = 11;
|
||||
private static $valuesFlag = array(
|
||||
self::FLAG_ENCRYPTION => 'encrypted file', // 1 << 0
|
||||
1 => 'compression option', // 1 << 1
|
||||
2 => 'compression option', // 1 << 2
|
||||
self::FLAG_DATA_DESCRIPTION => 'data descriptor', // 1 << 3
|
||||
4 => 'enhanced deflation', // 1 << 4
|
||||
5 => 'compressed patched data', // 1 << 5
|
||||
6 => 'strong encryption', // 1 << 6
|
||||
7 => 'unused', // 1 << 7
|
||||
8 => 'unused', // 1 << 8
|
||||
9 => 'unused', // 1 << 9
|
||||
10 => 'unused', // 1 << 10
|
||||
self::FLAG_UTF8 => 'language encoding', // 1 << 11
|
||||
12 => 'reserved', // 1 << 12
|
||||
13 => 'mask header values', // 1 << 13
|
||||
14 => 'reserved', // 1 << 14
|
||||
15 => 'reserved', // 1 << 15
|
||||
);
|
||||
|
||||
// compression method constants
|
||||
const COMPRESS_METHOD_STORED = 0;
|
||||
const COMPRESS_METHOD_DEFLATED = 8;
|
||||
const COMPRESS_METHOD_AES = 99;
|
||||
|
||||
private static $valuesCompressionMethod = array(
|
||||
self::COMPRESS_METHOD_STORED => 'no compression',
|
||||
1 => 'shrink',
|
||||
2 => 'reduce level 1',
|
||||
3 => 'reduce level 2',
|
||||
4 => 'reduce level 3',
|
||||
5 => 'reduce level 4',
|
||||
6 => 'implode',
|
||||
7 => 'reserved for Tokenizing compression algorithm',
|
||||
self::COMPRESS_METHOD_DEFLATED => 'deflate',
|
||||
9 => 'deflate64',
|
||||
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
|
||||
11 => 'reserved by PKWARE',
|
||||
12 => 'bzip2',
|
||||
13 => 'reserved by PKWARE',
|
||||
14 => 'LZMA (EFS)',
|
||||
15 => 'reserved by PKWARE',
|
||||
16 => 'reserved by PKWARE',
|
||||
17 => 'reserved by PKWARE',
|
||||
18 => 'IBM TERSE',
|
||||
19 => 'IBM LZ77 z Architecture (PFS)',
|
||||
97 => 'WavPack',
|
||||
98 => 'PPMd version I, Rev 1',
|
||||
self::COMPRESS_METHOD_AES => 'AES Encryption',
|
||||
);
|
||||
|
||||
const INTERNAL_ATTR_DEFAULT = 0;
|
||||
const EXTERNAL_ATTR_DEFAULT = 0;
|
||||
|
||||
/*
|
||||
* Extra field header ID
|
||||
*/
|
||||
const EXTID_ZIP64 = 0x0001; // Zip64
|
||||
const EXTID_NTFS = 0x000a; // NTFS (for storing full file times information)
|
||||
const EXTID_UNIX = 0x000d; // UNIX
|
||||
const EXTID_EXTT = 0x5455; // Info-ZIP Extended Timestamp
|
||||
const EXTID_UNICODE_FILENAME = 0x7075; // for Unicode filenames
|
||||
const EXTID_UNICODE_ = 0x6375; // for Unicode file comments
|
||||
const EXTID_STORING_STRINGS = 0x5A4C; // for storing strings code pages and Unicode filenames using custom Unicode implementation (see Unicode Support: Using Non-English Characters in Filenames, Comments and Passwords).
|
||||
const EXTID_OFFSETS_COMPRESS_DATA = 0x5A4D; // for saving offsets array from seekable compressed data
|
||||
const EXTID_AES_ENCRYPTION = 0x9901; // WinZip AES encryption (http://www.winzip.com/aes_info.htm)
|
||||
|
||||
/**
|
||||
* entry name
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* version made by
|
||||
* @var int
|
||||
*/
|
||||
private $versionMadeBy = self::MADE_BY_WINDOWS_NTFS;
|
||||
/**
|
||||
* version needed to extract
|
||||
* @var int
|
||||
*/
|
||||
private $versionExtract = self::EXTRACT_VERSION_20;
|
||||
/**
|
||||
* general purpose bit flag
|
||||
* @var int
|
||||
*/
|
||||
private $flag = 0;
|
||||
/**
|
||||
* compression method
|
||||
* @var int
|
||||
*/
|
||||
private $compressionMethod = self::COMPRESS_METHOD_DEFLATED;
|
||||
/**
|
||||
* last mod file datetime
|
||||
* @var int Unix timestamp
|
||||
*/
|
||||
private $lastModDateTime;
|
||||
/**
|
||||
* crc-32
|
||||
* @var int
|
||||
*/
|
||||
private $crc32;
|
||||
/**
|
||||
* compressed size
|
||||
* @var int
|
||||
*/
|
||||
private $compressedSize;
|
||||
/**
|
||||
* uncompressed size
|
||||
* @var int
|
||||
*/
|
||||
private $unCompressedSize;
|
||||
/**
|
||||
* disk number start
|
||||
* @var int
|
||||
*/
|
||||
private $diskNumber = 0;
|
||||
/**
|
||||
* internal file attributes
|
||||
* @var int
|
||||
*/
|
||||
private $internalAttributes = self::INTERNAL_ATTR_DEFAULT;
|
||||
/**
|
||||
* external file attributes
|
||||
* @var int
|
||||
*/
|
||||
private $externalAttributes = self::EXTERNAL_ATTR_DEFAULT;
|
||||
/**
|
||||
* relative offset of local header
|
||||
* @var int
|
||||
*/
|
||||
private $offsetOfLocal;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $offsetOfCentral;
|
||||
|
||||
/**
|
||||
* optional extra field data for entry
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $extraCentral = "";
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $extraLocal = "";
|
||||
/**
|
||||
* optional comment string for entry
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $comment = "";
|
||||
|
||||
function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function getLengthOfLocal()
|
||||
{
|
||||
return $this->getLengthLocalHeader() + $this->compressedSize + ($this->hasDataDescriptor() ? 12 : 0);
|
||||
}
|
||||
|
||||
public function getLengthLocalHeader()
|
||||
{
|
||||
return 30 + strlen($this->name) + strlen($this->extraLocal);
|
||||
}
|
||||
|
||||
public function getLengthOfCentral()
|
||||
{
|
||||
return 46 + strlen($this->name) + strlen($this->extraCentral) + strlen($this->comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Buffer $buffer
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function readCentralHeader(Buffer $buffer)
|
||||
{
|
||||
$signature = $buffer->getUnsignedInt(); // after offset 4
|
||||
if ($signature !== ZipFile::SIGNATURE_CENTRAL_DIR) {
|
||||
throw new ZipException("Can not read central directory. Bad signature: " . $signature);
|
||||
}
|
||||
$this->versionMadeBy = $buffer->getUnsignedShort(); // after offset 6
|
||||
$this->versionExtract = $buffer->getUnsignedShort(); // after offset 8
|
||||
$this->flag = $buffer->getUnsignedShort(); // after offset 10
|
||||
$this->compressionMethod = $buffer->getUnsignedShort(); // after offset 12
|
||||
$lastModTime = $buffer->getUnsignedShort(); // after offset 14
|
||||
$lastModDate = $buffer->getUnsignedShort(); // after offset 16
|
||||
$this->setLastModifyDosDatetime($lastModTime, $lastModDate);
|
||||
$this->crc32 = $buffer->getUnsignedInt(); // after offset 20
|
||||
$this->compressedSize = $buffer->getUnsignedInt(); // after offset 24
|
||||
$this->unCompressedSize = $buffer->getUnsignedInt(); // after offset 28
|
||||
$fileNameLength = $buffer->getUnsignedShort(); // after offset 30
|
||||
$extraCentralLength = $buffer->getUnsignedShort(); // after offset 32
|
||||
$fileCommentLength = $buffer->getUnsignedShort(); // after offset 34
|
||||
$this->diskNumber = $buffer->getUnsignedShort(); // after offset 36
|
||||
$this->internalAttributes = $buffer->getUnsignedShort(); // after offset 38
|
||||
$this->externalAttributes = $buffer->getUnsignedInt(); // after offset 42
|
||||
$this->offsetOfLocal = $buffer->getUnsignedInt(); // after offset 46
|
||||
$this->name = $buffer->getString($fileNameLength);
|
||||
$this->setExtra($buffer->getString($extraCentralLength));
|
||||
$this->comment = $buffer->getString($fileCommentLength);
|
||||
|
||||
$currentPos = $buffer->position();
|
||||
$buffer->setPosition($this->offsetOfLocal + 28);
|
||||
$extraLocalLength = $buffer->getUnsignedShort();
|
||||
$buffer->skip($fileNameLength);
|
||||
$this->extraLocal = $buffer->getString($extraLocalLength);
|
||||
$buffer->setPosition($currentPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional extra field data for the entry.
|
||||
*
|
||||
* @param string $extra the extra field data bytes
|
||||
* @throws ZipException
|
||||
*/
|
||||
private function setExtra($extra)
|
||||
{
|
||||
if (!empty($extra)) {
|
||||
$len = strlen($extra);
|
||||
if ($len > 0xFFFF) {
|
||||
throw new ZipException("invalid extra field length");
|
||||
}
|
||||
$buffer = new StringBuffer($extra);
|
||||
$buffer->setOrder(Buffer::LITTLE_ENDIAN);
|
||||
// extra fields are in "HeaderID(2)DataSize(2)Data... format
|
||||
while ($buffer->position() + 4 < $len) {
|
||||
$tag = $buffer->getUnsignedShort();
|
||||
$sz = $buffer->getUnsignedShort();
|
||||
if ($buffer->position() + $sz > $len) // invalid data
|
||||
break;
|
||||
switch ($tag) {
|
||||
case self::EXTID_ZIP64:
|
||||
// not support zip64
|
||||
break;
|
||||
case self::EXTID_NTFS:
|
||||
$buffer->skip(4); // reserved 4 bytes
|
||||
if ($buffer->getUnsignedShort() != 0x0001 || $buffer->getUnsignedShort() != 24)
|
||||
break;
|
||||
// $mtime = winTimeToFileTime($buffer->getLong());
|
||||
// $atime = winTimeToFileTime($buffer->getLong());
|
||||
// $ctime = winTimeToFileTime($buffer->getLong());
|
||||
break;
|
||||
case self::EXTID_EXTT:
|
||||
$flag = $buffer->getUnsignedByte();
|
||||
$sz0 = 1;
|
||||
// The CEN-header extra field contains the modification
|
||||
// time only, or no timestamp at all. 'sz' is used to
|
||||
// flag its presence or absence. But if mtime is present
|
||||
// in LOC it must be present in CEN as well.
|
||||
if (($flag & 0x1) != 0 && ($sz0 + 4) <= $sz) {
|
||||
$mtime = $buffer->getUnsignedInt();
|
||||
$sz0 += 4;
|
||||
}
|
||||
if (($flag & 0x2) != 0 && ($sz0 + 4) <= $sz) {
|
||||
$atime = $buffer->getUnsignedInt();
|
||||
$sz0 += 4;
|
||||
}
|
||||
if (($flag & 0x4) != 0 && ($sz0 + 4) <= $sz) {
|
||||
$ctime = $buffer->getUnsignedInt();
|
||||
$sz0 += 4;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->extraCentral = $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Buffer
|
||||
*/
|
||||
public function writeLocalHeader()
|
||||
{
|
||||
$buffer = new StringBuffer();
|
||||
$buffer->setOrder(Buffer::LITTLE_ENDIAN);
|
||||
$buffer->insertInt(ZipFile::SIGNATURE_LOCAL_HEADER);
|
||||
$buffer->insertShort($this->versionExtract);
|
||||
$buffer->insertShort($this->flag);
|
||||
$buffer->insertShort($this->compressionMethod);
|
||||
$buffer->insertShort($this->getLastModifyDosTime());
|
||||
$buffer->insertShort($this->getLastModifyDosDate());
|
||||
if ($this->hasDataDescriptor()) {
|
||||
$buffer->insertInt(0);
|
||||
$buffer->insertInt(0);
|
||||
$buffer->insertInt(0);
|
||||
} else {
|
||||
$buffer->insertInt($this->crc32);
|
||||
$buffer->insertInt($this->compressedSize);
|
||||
$buffer->insertInt($this->unCompressedSize);
|
||||
}
|
||||
$buffer->insertShort(strlen($this->name));
|
||||
$buffer->insertShort(strlen($this->extraLocal)); // offset 30
|
||||
$buffer->insertString($this->name);
|
||||
$buffer->insertString($this->extraLocal);
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $bit
|
||||
* @return bool
|
||||
*/
|
||||
public function setFlagBit($bit)
|
||||
{
|
||||
if ($bit < 0 || $bit > 15) {
|
||||
return false;
|
||||
}
|
||||
$this->flag |= 1 << $bit;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $bit
|
||||
* @return bool
|
||||
*/
|
||||
public function testFlagBit($bit)
|
||||
{
|
||||
return (($this->flag & (1 << $bit)) !== 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDataDescriptor()
|
||||
{
|
||||
return $this->testFlagBit(self::FLAG_DATA_DESCRIPTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEncrypted()
|
||||
{
|
||||
return $this->testFlagBit(self::FLAG_ENCRYPTION);
|
||||
}
|
||||
|
||||
public function writeDataDescriptor()
|
||||
{
|
||||
$buffer = new StringBuffer();
|
||||
$buffer->setOrder(Buffer::LITTLE_ENDIAN);
|
||||
$buffer->insertInt($this->crc32);
|
||||
$buffer->insertInt($this->compressedSize);
|
||||
$buffer->insertInt($this->unCompressedSize);
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Buffer
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function writeCentralHeader()
|
||||
{
|
||||
$buffer = new StringBuffer();
|
||||
$buffer->setOrder(Buffer::LITTLE_ENDIAN);
|
||||
|
||||
$buffer->insertInt(ZipFile::SIGNATURE_CENTRAL_DIR);
|
||||
$buffer->insertShort($this->versionMadeBy);
|
||||
$buffer->insertShort($this->versionExtract);
|
||||
$buffer->insertShort($this->flag);
|
||||
$buffer->insertShort($this->compressionMethod);
|
||||
$buffer->insertShort($this->getLastModifyDosTime());
|
||||
$buffer->insertShort($this->getLastModifyDosDate());
|
||||
$buffer->insertInt($this->crc32);
|
||||
$buffer->insertInt($this->compressedSize);
|
||||
$buffer->insertInt($this->unCompressedSize);
|
||||
$buffer->insertShort(strlen($this->name));
|
||||
$buffer->insertShort(strlen($this->extraCentral));
|
||||
$buffer->insertShort(strlen($this->comment));
|
||||
$buffer->insertShort($this->diskNumber);
|
||||
$buffer->insertShort($this->internalAttributes);
|
||||
$buffer->insertInt($this->externalAttributes);
|
||||
$buffer->insertInt($this->offsetOfLocal);
|
||||
$buffer->insertString($this->name);
|
||||
$buffer->insertString($this->extraCentral);
|
||||
$buffer->insertString($this->comment);
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDirectory()
|
||||
{
|
||||
return $this->name[strlen($this->name) - 1] === "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getValuesMadeBy()
|
||||
{
|
||||
return self::$valuesMadeBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $valuesMadeBy
|
||||
*/
|
||||
public static function setValuesMadeBy($valuesMadeBy)
|
||||
{
|
||||
self::$valuesMadeBy = $valuesMadeBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getValuesFlag()
|
||||
{
|
||||
return self::$valuesFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $valuesFlag
|
||||
*/
|
||||
public static function setValuesFlag($valuesFlag)
|
||||
{
|
||||
self::$valuesFlag = $valuesFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getValuesCompressionMethod()
|
||||
{
|
||||
return self::$valuesCompressionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $valuesCompressionMethod
|
||||
*/
|
||||
public static function setValuesCompressionMethod($valuesCompressionMethod)
|
||||
{
|
||||
self::$valuesCompressionMethod = $valuesCompressionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
if (strlen($name) > 0xFFFF) {
|
||||
throw new ZipException("entry name too long");
|
||||
}
|
||||
$this->name = $name;
|
||||
$encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true);
|
||||
if ($encoding === 'UTF-8') {
|
||||
$this->setFlagBit(self::FLAG_UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getVersionMadeBy()
|
||||
{
|
||||
return $this->versionMadeBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $versionMadeBy
|
||||
*/
|
||||
public function setVersionMadeBy($versionMadeBy)
|
||||
{
|
||||
$this->versionMadeBy = $versionMadeBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getVersionExtract()
|
||||
{
|
||||
return $this->versionExtract;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $versionExtract
|
||||
*/
|
||||
public function setVersionExtract($versionExtract)
|
||||
{
|
||||
$this->versionExtract = $versionExtract;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getFlag()
|
||||
{
|
||||
return $this->flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $flag
|
||||
*/
|
||||
public function setFlag($flag)
|
||||
{
|
||||
$this->flag = $flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCompressionMethod()
|
||||
{
|
||||
return $this->compressionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $compressionMethod
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setCompressionMethod($compressionMethod)
|
||||
{
|
||||
if (!isset(self::$valuesCompressionMethod[$compressionMethod])) {
|
||||
throw new ZipException("invalid compression method " . $compressionMethod);
|
||||
}
|
||||
$this->compressionMethod = $compressionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastModDateTime()
|
||||
{
|
||||
return $this->lastModDateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $lastModDateTime
|
||||
*/
|
||||
public function setLastModDateTime($lastModDateTime)
|
||||
{
|
||||
$this->lastModDateTime = $lastModDateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCrc32()
|
||||
{
|
||||
return $this->crc32;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $crc32
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setCrc32($crc32)
|
||||
{
|
||||
if ($crc32 < 0 || $crc32 > 0xFFFFFFFF) {
|
||||
throw new ZipException("invalid entry crc-32");
|
||||
}
|
||||
$this->crc32 = $crc32;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCompressedSize()
|
||||
{
|
||||
return $this->compressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $compressedSize
|
||||
*/
|
||||
public function setCompressedSize($compressedSize)
|
||||
{
|
||||
$this->compressedSize = $compressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUnCompressedSize()
|
||||
{
|
||||
return $this->unCompressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $unCompressedSize
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setUnCompressedSize($unCompressedSize)
|
||||
{
|
||||
if ($unCompressedSize < 0 || $unCompressedSize > 0xFFFFFFFF) {
|
||||
throw new ZipException("invalid entry size");
|
||||
}
|
||||
$this->unCompressedSize = $unCompressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDiskNumber()
|
||||
{
|
||||
return $this->diskNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $diskNumber
|
||||
*/
|
||||
public function setDiskNumber($diskNumber)
|
||||
{
|
||||
$this->diskNumber = $diskNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getInternalAttributes()
|
||||
{
|
||||
return $this->internalAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $internalAttributes
|
||||
*/
|
||||
public function setInternalAttributes($internalAttributes)
|
||||
{
|
||||
$this->internalAttributes = $internalAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getExternalAttributes()
|
||||
{
|
||||
return $this->externalAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $externalAttributes
|
||||
*/
|
||||
public function setExternalAttributes($externalAttributes)
|
||||
{
|
||||
$this->externalAttributes = $externalAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOffsetOfLocal()
|
||||
{
|
||||
return $this->offsetOfLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offsetOfLocal
|
||||
*/
|
||||
public function setOffsetOfLocal($offsetOfLocal)
|
||||
{
|
||||
$this->offsetOfLocal = $offsetOfLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOffsetOfCentral()
|
||||
{
|
||||
return $this->offsetOfCentral;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offsetOfCentral
|
||||
*/
|
||||
public function setOffsetOfCentral($offsetOfCentral)
|
||||
{
|
||||
$this->offsetOfCentral = $offsetOfCentral;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExtraCentral()
|
||||
{
|
||||
return $this->extraCentral;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extra
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setExtraCentral($extra)
|
||||
{
|
||||
if ($extra !== null && strlen($extra) > 0xFFFF) {
|
||||
throw new ZipException("invalid extra field length");
|
||||
}
|
||||
$this->extraCentral = $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extra
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function setExtraLocal($extra)
|
||||
{
|
||||
if ($extra !== null && strlen($extra) > 0xFFFF) {
|
||||
throw new ZipException("invalid extra field length");
|
||||
}
|
||||
$this->extraLocal = $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExtraLocal()
|
||||
{
|
||||
return $this->extraLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comment
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $lastModTime
|
||||
* @param int $lastModDate
|
||||
*/
|
||||
private function setLastModifyDosDatetime($lastModTime, $lastModDate)
|
||||
{
|
||||
$hour = ($lastModTime & 0xF800) >> 11;
|
||||
$minute = ($lastModTime & 0x07E0) >> 5;
|
||||
$seconds = ($lastModTime & 0x001F) * 2;
|
||||
|
||||
$year = (($lastModDate & 0xFE00) >> 9) + 1980;
|
||||
$month = ($lastModDate & 0x01E0) >> 5;
|
||||
$day = $lastModDate & 0x001F;
|
||||
|
||||
// ----- Get UNIX date format
|
||||
$this->lastModDateTime = mktime($hour, $minute, $seconds, $month, $day, $year);
|
||||
}
|
||||
|
||||
public function getLastModifyDosTime()
|
||||
{
|
||||
$date = getdate($this->lastModDateTime);
|
||||
return ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2;
|
||||
}
|
||||
|
||||
public function getLastModifyDosDate()
|
||||
{
|
||||
$date = getdate($this->lastModDateTime);
|
||||
return (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday'];
|
||||
}
|
||||
|
||||
public function versionMadeToString()
|
||||
{
|
||||
if (isset(self::$valuesMadeBy[$this->versionMadeBy])) {
|
||||
return self::$valuesMadeBy[$this->versionMadeBy];
|
||||
} else return "unknown";
|
||||
}
|
||||
|
||||
public function compressionMethodToString()
|
||||
{
|
||||
if (isset(self::$valuesCompressionMethod[$this->compressionMethod])) {
|
||||
return self::$valuesCompressionMethod[$this->compressionMethod];
|
||||
} else return "unknown";
|
||||
}
|
||||
|
||||
public function flagToString()
|
||||
{
|
||||
$return = array();
|
||||
foreach (self::$valuesFlag AS $bit => $value) {
|
||||
if ($this->testFlagBit($bit)) {
|
||||
$return[] = $value;
|
||||
}
|
||||
}
|
||||
if (!empty($return)) {
|
||||
return implode(', ', $return);
|
||||
} else if ($this->flag === 0) {
|
||||
return "default";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function __toString()
|
||||
{
|
||||
return __CLASS__ . '{' .
|
||||
'name="' . $this->name . '"' .
|
||||
', versionMadeBy={' . $this->versionMadeBy . ' => "' . $this->versionMadeToString() . '"}' .
|
||||
', versionExtract="' . $this->versionExtract . '"' .
|
||||
', flag={' . $this->flag . ' => ' . $this->flagToString() . '}' .
|
||||
', compressionMethod={' . $this->compressionMethod . ' => ' . $this->compressionMethodToString() . '}' .
|
||||
', lastModify=' . date("Y-m-d H:i:s", $this->lastModDateTime) .
|
||||
', crc32=0x' . dechex($this->crc32) .
|
||||
', compressedSize=' . ZipUtils::humanSize($this->compressedSize) .
|
||||
', unCompressedSize=' . ZipUtils::humanSize($this->unCompressedSize) .
|
||||
', diskNumber=' . $this->diskNumber .
|
||||
', internalAttributes=' . $this->internalAttributes .
|
||||
', externalAttributes=' . $this->externalAttributes .
|
||||
', offsetOfLocal=' . $this->offsetOfLocal .
|
||||
', offsetOfCentral=' . $this->offsetOfCentral .
|
||||
', extraCentral="' . $this->extraCentral . '"' .
|
||||
', extraLocal="' . $this->extraLocal . '"' .
|
||||
', comment="' . $this->comment . '"' .
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
namespace Nelexa\Zip;
|
||||
|
||||
class ZipException extends \Exception
|
||||
{
|
||||
|
||||
}
|
1374
src/ZipFile.php
1374
src/ZipFile.php
File diff suppressed because it is too large
Load Diff
104
src/ZipUtils.php
104
src/ZipUtils.php
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
namespace Nelexa\Zip;
|
||||
|
||||
use Nelexa\Buffer\MathHelper;
|
||||
|
||||
class ZipUtils
|
||||
{
|
||||
const DECRYPT_HEADER_SIZE = 12;
|
||||
public static $CFH_SIGNATURE = array(0x50, 0x4b, 0x01, 0x02);
|
||||
public static $LFH_SIGNATURE = array(0x50, 0x4b, 0x03, 0x04);
|
||||
public static $ECD_SIGNATURE = array(0x50, 0x4b, 0x05, 0x06);
|
||||
public static $DD_SIGNATURE = array(0x50, 0x4b, 0x07, 0x08);
|
||||
|
||||
private static $CRC_TABLE = array(
|
||||
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
|
||||
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
||||
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
|
||||
0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
|
||||
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
|
||||
0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
|
||||
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
|
||||
0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
|
||||
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
|
||||
0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
|
||||
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
|
||||
0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
|
||||
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
|
||||
0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
|
||||
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
|
||||
0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
|
||||
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
|
||||
0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
|
||||
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
|
||||
0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
|
||||
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
|
||||
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
|
||||
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
|
||||
0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
|
||||
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
|
||||
0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
|
||||
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
|
||||
0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
|
||||
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
|
||||
0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
|
||||
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
|
||||
0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
|
||||
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
|
||||
0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
|
||||
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
|
||||
0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
|
||||
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
|
||||
0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
|
||||
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
|
||||
0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
|
||||
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
|
||||
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
||||
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param int $charAt
|
||||
* @param int[] $keys
|
||||
* @return array
|
||||
*/
|
||||
public static function updateKeys($charAt, array $keys)
|
||||
{
|
||||
$keys[0] = self::crc32($keys[0], $charAt);
|
||||
$keys[1] = MathHelper::add($keys[1], MathHelper::bitwiseAnd($keys[0], 0xff));
|
||||
$keys[1] = MathHelper::castToInt(MathHelper::add(MathHelper::mul($keys[1], 134775813), 1));
|
||||
$keys[2] = self::crc32($keys[2], MathHelper::rightShift32($keys[1], 24));
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alg: ((oldCrc >>> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff])
|
||||
*
|
||||
* @param int $oldCrc
|
||||
* @param int $charAt
|
||||
* @return int|string
|
||||
*/
|
||||
public static function crc32($oldCrc, $charAt)
|
||||
{
|
||||
return MathHelper::castToInt(MathHelper::bitwiseXor(MathHelper::unsignedRightShift32($oldCrc, 8), self::$CRC_TABLE[MathHelper::bitwiseAnd(MathHelper::bitwiseXor($oldCrc, $charAt), 0xff)]));
|
||||
}
|
||||
|
||||
public static function decryptByte($byte)
|
||||
{
|
||||
$temp = $byte | 2;
|
||||
return MathHelper::unsignedRightShift32($temp * ($temp ^ 1), 8);
|
||||
}
|
||||
|
||||
public static function humanSize($size, $unit = "")
|
||||
{
|
||||
if ((!$unit && $size >= 1 << 30) || $unit == "GB")
|
||||
return number_format($size / (1 << 30), 2) . "GB";
|
||||
if ((!$unit && $size >= 1 << 20) || $unit == "MB")
|
||||
return number_format($size / (1 << 20), 2) . "MB";
|
||||
if ((!$unit && $size >= 1 << 10) || $unit == "KB")
|
||||
return number_format($size / (1 << 10), 2) . "KB";
|
||||
return number_format($size) . " bytes";
|
||||
}
|
||||
|
||||
|
||||
}
|
1093
tests/PhpZip/ZipTest.php
Normal file
1093
tests/PhpZip/ZipTest.php
Normal file
File diff suppressed because it is too large
Load Diff
89
tests/PhpZip/ZipTestCase.php
Normal file
89
tests/PhpZip/ZipTestCase.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
/**
|
||||
* PHPUnit test case and helper methods.
|
||||
*/
|
||||
class ZipTestCase extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* Assert correct zip archive.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string|null $password
|
||||
*/
|
||||
public static function assertCorrectZipArchive($filename, $password = null)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
self::assertNotContains('incorrect password', $output);
|
||||
self::assertContains(' OK', $output);
|
||||
self::assertContains('No errors', $output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert correct empty zip archive.
|
||||
*
|
||||
* @param $filename
|
||||
*/
|
||||
public static function assertCorrectEmptyZip($filename)
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR !== '\\' && `which zipinfo`) {
|
||||
exec("zipinfo " . escapeshellarg($filename), $output, $returnCode);
|
||||
|
||||
$output = implode(PHP_EOL, $output);
|
||||
|
||||
self::assertContains('Empty zipfile', $output);
|
||||
}
|
||||
$actualEmptyZipData = pack('VVVVVv', ZipConstants::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
|
||||
self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return bool|null If null - can not install zipalign
|
||||
*/
|
||||
public static function doZipAlignVerify($filename)
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
|
||||
exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
|
||||
return $returnCode === 0;
|
||||
} else {
|
||||
fwrite(STDERR, 'Can not find program "zipalign" for test');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,244 +0,0 @@
|
||||
<?php
|
||||
|
||||
class TestZipFile extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testCreate()
|
||||
{
|
||||
$output = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create.zip';
|
||||
$extractOutputDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create';
|
||||
|
||||
$listFilename = 'files.txt';
|
||||
$listFileContent = implode(PHP_EOL, glob('*'));
|
||||
$dirName = 'src/';
|
||||
$archiveComment = 'Archive comment - 😀';
|
||||
$commentIndex0 = basename(__FILE__);
|
||||
|
||||
$zip = new \Nelexa\Zip\ZipFile();
|
||||
$zip->create();
|
||||
$zip->addFile(__FILE__);
|
||||
$zip->addFromString($listFilename, $listFileContent);
|
||||
$zip->addEmptyDir($dirName);
|
||||
$zip->setArchiveComment($archiveComment);
|
||||
$zip->setCommentIndex(0, $commentIndex0);
|
||||
$zip->saveAs($output);
|
||||
$zip->close();
|
||||
|
||||
$this->assertTrue(file_exists($output));
|
||||
$this->assertCorrectZipArchive($output);
|
||||
|
||||
$zip = new \Nelexa\Zip\ZipFile();
|
||||
$zip->open($output);
|
||||
$listFiles = $zip->getListFiles();
|
||||
|
||||
$this->assertEquals(sizeof($listFiles), 3);
|
||||
$filenameIndex0 = basename(__FILE__);
|
||||
$this->assertEquals($listFiles[0], $filenameIndex0);
|
||||
$this->assertEquals($listFiles[1], $listFilename);
|
||||
$this->assertEquals($listFiles[2], $dirName);
|
||||
|
||||
$this->assertEquals($zip->getFromIndex(0), $zip->getFromName(basename(__FILE__)));
|
||||
$this->assertEquals($zip->getFromIndex(0), file_get_contents(__FILE__));
|
||||
$this->assertEquals($zip->getFromIndex(1), $zip->getFromName($listFilename));
|
||||
$this->assertEquals($zip->getFromIndex(1), $listFileContent);
|
||||
|
||||
$this->assertEquals($zip->getArchiveComment(), $archiveComment);
|
||||
$this->assertEquals($zip->getCommentIndex(0), $commentIndex0);
|
||||
|
||||
if (!file_exists($extractOutputDir)) {
|
||||
$this->assertTrue(mkdir($extractOutputDir, 0755, true));
|
||||
}
|
||||
|
||||
$zip->extractTo($extractOutputDir);
|
||||
|
||||
$this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0));
|
||||
$this->assertEquals(md5_file($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0), md5_file(__FILE__));
|
||||
|
||||
$this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename));
|
||||
$this->assertEquals(file_get_contents($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename), $listFileContent);
|
||||
|
||||
$zip->close();
|
||||
|
||||
unlink($output);
|
||||
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($extractOutputDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($files as $fileInfo) {
|
||||
$todo = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
|
||||
$todo($fileInfo->getRealPath());
|
||||
}
|
||||
|
||||
rmdir($extractOutputDir);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function testUpdate()
|
||||
{
|
||||
$file = __DIR__ . '/res/file.apk';
|
||||
$privateKey = __DIR__ . '/res/private.pem';
|
||||
$publicKey = __DIR__ . '/res/public.pem';
|
||||
$outputFile = sys_get_temp_dir() . '/test-update.apk';
|
||||
|
||||
$zip = new \Nelexa\Zip\ZipFile($file);
|
||||
$zip->open($file);
|
||||
|
||||
// signed apk file
|
||||
$certList = array();
|
||||
$manifestMf = new Manifest();
|
||||
$manifestMf->appendLine("Manifest-Version: 1.0");
|
||||
$manifestMf->appendLine("Created-By: 1.0 (Android)");
|
||||
$manifestMf->appendLine('');
|
||||
for ($i = 0, $length = $zip->getCountFiles(); $i < $length; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if ($name[strlen($name) - 1] === '/') continue; // is path
|
||||
$content = $zip->getFromIndex($i);
|
||||
|
||||
$certManifest = $this->createSha1EncodeEntryManifest($name, $content);
|
||||
$manifestMf->appendManifest($certManifest);
|
||||
$certList[$name] = $certManifest;
|
||||
}
|
||||
$manifestMf = $manifestMf->getContent();
|
||||
|
||||
$certSf = new Manifest();
|
||||
$certSf->appendLine('Signature-Version: 1.0');
|
||||
$certSf->appendLine('Created-By: 1.0 (Android)');
|
||||
$certSf->appendLine('SHA1-Digest-Manifest: ' . base64_encode(sha1($manifestMf, 1)));
|
||||
$certSf->appendLine('');
|
||||
foreach ($certList AS $filename => $content) {
|
||||
$certManifest = $this->createSha1EncodeEntryManifest($filename, $content->getContent());
|
||||
$certSf->appendManifest($certManifest);
|
||||
}
|
||||
$certSf = $certSf->getContent();
|
||||
unset($certList);
|
||||
|
||||
$zip->addFromString('META-INF/MANIFEST.MF', $manifestMf);
|
||||
$zip->addFromString('META-INF/CERT.SF', $certSf);
|
||||
|
||||
if (`which openssl`) {
|
||||
$openssl_cmd = 'printf ' . escapeshellarg($certSf) . ' | openssl smime -md sha1 -sign -inkey ' . escapeshellarg($privateKey) . ' -signer ' . $publicKey . ' -binary -outform DER -noattr';
|
||||
|
||||
ob_start();
|
||||
passthru($openssl_cmd, $error);
|
||||
$rsaContent = ob_get_clean();
|
||||
$this->assertEquals($error, 0);
|
||||
|
||||
$zip->addFromString('META-INF/CERT.RSA', $rsaContent);
|
||||
}
|
||||
|
||||
$zip->saveAs($outputFile);
|
||||
$zip->close();
|
||||
|
||||
$this->assertCorrectZipArchive($outputFile);
|
||||
|
||||
if (`which jarsigner`) {
|
||||
ob_start();
|
||||
passthru('jarsigner -verify -verbose -certs ' . escapeshellarg($outputFile), $error);
|
||||
$verifedResult = ob_get_clean();
|
||||
|
||||
$this->assertEquals($error, 0);
|
||||
$this->assertContains('jar verified', $verifedResult);
|
||||
}
|
||||
|
||||
unlink($outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filename
|
||||
*/
|
||||
private function assertCorrectZipArchive($filename)
|
||||
{
|
||||
exec("zip -T " . escapeshellarg($filename), $output, $returnCode);
|
||||
$this->assertEquals($returnCode, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param string $content
|
||||
* @return Manifest
|
||||
*/
|
||||
private function createSha1EncodeEntryManifest($filename, $content)
|
||||
{
|
||||
$manifest = new Manifest();
|
||||
$manifest->appendLine('Name: ' . $filename);
|
||||
$manifest->appendLine('SHA1-Digest: ' . base64_encode(sha1($content, 1)));
|
||||
return $manifest;
|
||||
}
|
||||
}
|
||||
|
||||
class Manifest
|
||||
{
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return trim($this->content) . "\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a long manifest line and add continuation if required
|
||||
* @param $line string
|
||||
* @return Manifest
|
||||
*/
|
||||
public function appendLine($line)
|
||||
{
|
||||
$begin = 0;
|
||||
$sb = '';
|
||||
$lineLength = mb_strlen($line, "UTF-8");
|
||||
for ($end = 70; $lineLength - $begin > 70; $end += 69) {
|
||||
$sb .= mb_substr($line, $begin, $end - $begin, "UTF-8") . "\r\n ";
|
||||
$begin = $end;
|
||||
}
|
||||
$this->content .= $sb . mb_substr($line, $begin, $lineLength, "UTF-8") . "\r\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendManifest(Manifest $manifest)
|
||||
{
|
||||
$this->content .= $manifest->getContent();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->content = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $manifestContent
|
||||
* @return Manifest
|
||||
*/
|
||||
public static function createFromManifest($manifestContent)
|
||||
{
|
||||
$manifestContent = trim($manifestContent);
|
||||
$lines = explode("\n", $manifestContent);
|
||||
|
||||
// normalize manifest
|
||||
$content = '';
|
||||
$trim = array("\r", "\n");
|
||||
foreach ($lines AS $line) {
|
||||
|
||||
$line = str_replace($trim, '', $line);
|
||||
if ($line[0] === ' ') {
|
||||
$content = rtrim($content, "\n\r");
|
||||
$line = ltrim($line);
|
||||
}
|
||||
$content .= $line . "\r\n";
|
||||
}
|
||||
|
||||
$manifset = new self;
|
||||
$lines = explode("\n", $content);
|
||||
foreach ($lines AS $line) {
|
||||
$line = trim($line, "\n\r");
|
||||
$manifset->appendLine($line);
|
||||
}
|
||||
return $manifset;
|
||||
}
|
||||
}
|
Binary file not shown.
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwiURw5w8TAS0G
|
||||
iWaBzJqbQyacFsIzKc+orHDzBdBdKlrx/aUlw3fBA310aP7343hc7vMm6koar/1n
|
||||
8iEh2W4retDHzNf9j3IXETa5/D+3WJY4aVJkYcFr8v6OEnuSXIwKZduMeL4BpQwM
|
||||
Xu/z6gkoTa9o+Dzhl46l71UX8umdPIx/4YWwX2oSm6EGklcGJyYdqMvwOXVXDE/J
|
||||
qqPzC7ZQiu422cDDvqBYt32CVyjLKCo5YjWBb24jxjtl5M4y8xPOcHlVEG2WwPBU
|
||||
sv2aw4Z9/Q0erZ35gMyzu+Y+2623B3tuAbfsswgBINq0bl2v+M17Kpv2tjhMKj1H
|
||||
ds6CK/BVAgMBAAECggEAdTUt86f1IjENq+Fd5Z/qplsXL1sM5NtFvD+BXljl1nVg
|
||||
nHpDQ6dbwxKGINv1LLAiIdGkLpovSTi/jlv8E3VA6C1KoN0oKnkqzpXnN+R6iUiP
|
||||
tDR5N5yPxxQ2Xi13Td2UPPMTqVghDwZ90VjXB6LDIbcyVwc5pK3zT8hvPs9Qu8t1
|
||||
S2pCEKcowvTRSB1DMTZ3lrNjEEIMdV0H8Qik3lf7ognRGoDywu5pA1bc/Yg+XlmP
|
||||
/ZmQinFeg3izNQzDdP6Ppo1i/QFeVXVuMs2ergMMHJRNUhBXKz8iNyVupqfroE8a
|
||||
xRpD3eO+KvSNb0TJR5TXf64t62zEEpHaRsmgACEMAQKBgQDXo0jVUa67oqfJBFBU
|
||||
3zfHIlgcrydRE4Av+LJ0hdEnFpMwVpUihJXUaJijawTzTKOgpVImhxfr/T1HMalm
|
||||
MTXH5Tc7inJTiB9A1IffLPqgoOr2JRwQ2q8lgWkQPkq1ySd+q0vhkj1tuAe3qI1i
|
||||
jiMo1Vb9zdVjcxmvPnZRKJgiIQKBgQDRlFm6PKc2Zx46BXeNPtXnHhSduUBJf2iO
|
||||
n9/pKTANQuDlPwC3Q4edSKe44fZ/oj4KRAnzX254wXBMX+ktKX/kqXbwEanxcd/v
|
||||
Lnvgv8QhsEKO3Ye09yasAfC2lYsSVSwHv+dYurb0nZ2JEPL1IP+V76RgTbdeMdic
|
||||
Mt53jN/vtQKBgQC+D+mOO+Sq9X61qtuzMtvS5O6MucUJrQp7PdTs51WmAjvRiz7/
|
||||
oaT+BwMiZp2CZLaETbLOypvHIPn12kvZCt7ARcQc8rY58ey6E5l+mAJ/udXfBm5q
|
||||
XJWrlRipfH4VJCtvdkP3mhIStvX2ZtXXXDiZMRDvu5CtizHESGW4uvL8gQKBgCI7
|
||||
ukBagfG3/E7776BJwETlO/bbeK3Iuvp5EOkUCj5QS04G8YX96Nv/Ly5a8pm8lae1
|
||||
n256ix/8cOx4yizPV42xRLVIHVtL/4khLajzigT6tpSBiRY9PLriAkDAwpu2/98w
|
||||
MIjkzte8Gyx1cUorHrSOFWqJp0cim0BAauhaQYX1AoGAPvb5TG/A6LhYKgCWUMOH
|
||||
lucrnV3Ns1BzaaMmOaokuYGtyTv2loVlrv+7QGdC9UBRXDz46DTE7CPHBwReNnWB
|
||||
R7YW50VwB9YfD7dqRM24Y08F3B7RCNhqsAnpAtVgXf+/01o2nfJbzxTty/STiBNB
|
||||
OjjxKHnAgIfhe7xAIiY2eow=
|
||||
-----END PRIVATE KEY-----
|
@@ -1,21 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDeTCCAmGgAwIBAgIEDeVWNjANBgkqhkiG9w0BAQUFADBtMQswCQYDVQQGEwJT
|
||||
QTEPMA0GA1UECBMGUml5YWRoMQ8wDQYDVQQHEwZSaXlhZGgxEDAOBgNVBAoTB1lv
|
||||
dXR5cGUxEDAOBgNVBAsTB1lvdXR5cGUxGDAWBgNVBAMTD0x1Y2llbm5lIEFuc2Vs
|
||||
YTAeFw0xNjA5MDgxNDM2MjJaFw00NDAxMjUxNDM2MjJaMG0xCzAJBgNVBAYTAlNB
|
||||
MQ8wDQYDVQQIEwZSaXlhZGgxDzANBgNVBAcTBlJpeWFkaDEQMA4GA1UEChMHWW91
|
||||
dHlwZTEQMA4GA1UECxMHWW91dHlwZTEYMBYGA1UEAxMPTHVjaWVubmUgQW5zZWxh
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsIlEcOcPEwEtBolmgcya
|
||||
m0MmnBbCMynPqKxw8wXQXSpa8f2lJcN3wQN9dGj+9+N4XO7zJupKGq/9Z/IhIdlu
|
||||
K3rQx8zX/Y9yFxE2ufw/t1iWOGlSZGHBa/L+jhJ7klyMCmXbjHi+AaUMDF7v8+oJ
|
||||
KE2vaPg84ZeOpe9VF/LpnTyMf+GFsF9qEpuhBpJXBicmHajL8Dl1VwxPyaqj8wu2
|
||||
UIruNtnAw76gWLd9glcoyygqOWI1gW9uI8Y7ZeTOMvMTznB5VRBtlsDwVLL9msOG
|
||||
ff0NHq2d+YDMs7vmPtuttwd7bgG37LMIASDatG5dr/jNeyqb9rY4TCo9R3bOgivw
|
||||
VQIDAQABoyEwHzAdBgNVHQ4EFgQUEPoIQyYzpjseEK7hqm6UALvjJj8wDQYJKoZI
|
||||
hvcNAQEFBQADggEBAD/C/48B4MvF2WzhMtLIAWuhtp73xBy6GCQBKT1dn9dgtXfD
|
||||
LuHAvkx28CoOTso4Ia+JhWuu7jGfYdtL00ezV8d8Ma1k/SJfWyHpgDDk1MEhvn+h
|
||||
tOoUQpt0S+QhKFxDm+INv2zw/P/TDIIodHQqkX+YVSLQMhUGRTq3vhDnfJqedAUr
|
||||
QIhZjCZx9VjjiM4yhcabKEHpxqLQOcoeHB8zchnP1j/N+QSIW6hICqjcPLPLzpPu
|
||||
M0RmEuRYz3EJ2P3jINhaCLFRLHTnoN2lVDS32v5Cr+IC7A1hPUcHG+07junRMEiG
|
||||
uTYj9+UYI6phGJBABfFp7/oxs080RXCrKUhR+Go=
|
||||
-----END CERTIFICATE-----
|
Reference in New Issue
Block a user