mirror of
https://github.com/Ne-Lexa/php-zip.git
synced 2025-01-17 12:48:28 +01:00
Completely rewritten code.
Implement read-only zip file - class \PhpZip\ZipFile, create and update zip file - \PhpZip\ZipOutputFile. Supports ZIP64 ext, Traditional PKWARE Encryption, WinZip AES Encryption.
This commit is contained in:
parent
5c23e588ff
commit
560a94c910
550
README.md
550
README.md
@ -1,249 +1,455 @@
|
||||
## 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`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
|
||||
|
||||
### class \Nelexa\Zip\ZipFile
|
||||
Initialization
|
||||
The library does not require extension `php-xml` 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", 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
|
||||
//)
|
||||
```
|
||||
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);
|
||||
```
|
||||
Close zip archive.
|
||||
```php
|
||||
$zipFile->close();
|
||||
```
|
||||
#### Class `\PhpZip\ZipOutputFile` (create, update, extract)
|
||||
Create zip archive.
|
||||
```php
|
||||
$zipOutputFile = new \PhpZip\ZipOutputFile();
|
||||
// or
|
||||
$zipOutputFile = \PhpZip\ZipOutputFile::create();
|
||||
```
|
||||
Open zip file from update.
|
||||
```php
|
||||
// initial ZipFile
|
||||
$zipFile = \PhpZip\ZipFile::openFromFile($filename);
|
||||
|
||||
// Create output stream from update zip file
|
||||
$zipOutputFile = new \PhpZip\ZipOutputFile($zipFile);
|
||||
// or
|
||||
$zipOutputFile = \PhpZip\ZipOutputFile::openFromZipFile($zipFile);
|
||||
```
|
||||
Add entry from file.
|
||||
```php
|
||||
$zipOutputFile->addFromFile($filename); // $entryName == basename($filename);
|
||||
$zipOutputFile->addFromFile($filename, $entryName);
|
||||
$zipOutputFile->addFromFile($filename, $entryName, ZipEntry::METHOD_DEFLATED);
|
||||
$zipOutputFile->addFromFile($filename, null, ZipEntry::METHOD_BZIP2); // $entryName == basename($filename);
|
||||
```
|
||||
Add entry from string data.
|
||||
```php
|
||||
$zipOutputFile->addFromString($entryName, $data)
|
||||
$zipOutputFile->addFromString($entryName, $data, ZipEntry::METHOD_DEFLATED)
|
||||
```
|
||||
Add entry from stream.
|
||||
```php
|
||||
$zipOutputFile->addFromStream($stream, $entryName)
|
||||
$zipOutputFile->addFromStream($stream, $entryName, ZipEntry::METHOD_DEFLATED)
|
||||
```
|
||||
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->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();
|
||||
}
|
||||
```
|
||||
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 = \PhpZip\ZipOutputFile::openFromZipFile($zipFile); // create zip output archive for update
|
||||
$zipOutputFile->deleteFromRegex('~^\.~'); // delete all hidden (Unix) files
|
||||
$zipOutputFile->addFromString('dir/file.txt', 'Test file'); // add files from string contents
|
||||
$zipOutputFile->saveAsFile($outputFilename); // update zip file
|
||||
$zipOutputFile->close(); // close output file, release all streams
|
||||
|
||||
$zipFile->close(); // close input file, release all streams
|
||||
```
|
||||
Other examples can be found in the `tests/` folder
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
```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,7 +1,13 @@
|
||||
{
|
||||
"name": "nelexa/zip",
|
||||
"description": "Zip create, modify and extract tool. Alternative ZipArchive.",
|
||||
"description": "Zip files CRUD. Open, create, update, extract and get info tool. Support read and write encrypted archives. Support ZIP64 ext. Alternative ZipArchive. It does not require php-zip extension.",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"zip",
|
||||
"archive",
|
||||
"extract",
|
||||
"winzip"
|
||||
],
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.8"
|
||||
},
|
||||
@ -9,17 +15,23 @@
|
||||
"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/Nelexa/Zip"
|
||||
"": "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);
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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";
|
||||
}
|
||||
|
||||
|
||||
}
|
215
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php
Normal file
215
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php
Normal file
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\CryptoUtil;
|
||||
|
||||
/**
|
||||
* Traditional PKWARE Encryption Engine.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class TraditionalPkwareEncryptionEngine
|
||||
{
|
||||
/**
|
||||
* Encryption header size
|
||||
*/
|
||||
const STD_DEC_HDR_SIZE = 12;
|
||||
|
||||
/**
|
||||
* Crc table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $CRC_TABLE = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
|
||||
];
|
||||
|
||||
/**
|
||||
* Encryption keys
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $keys = [];
|
||||
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* ZipCryptoEngine constructor.
|
||||
*
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$this->initKeys($entry->getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial keys
|
||||
*
|
||||
* @param string $password
|
||||
*/
|
||||
private function initKeys($password)
|
||||
{
|
||||
$this->keys[0] = 305419896;
|
||||
$this->keys[1] = 591751049;
|
||||
$this->keys[2] = 878082192;
|
||||
foreach (unpack('C*', $password) as $b) {
|
||||
$this->updateKeys($b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update keys.
|
||||
*
|
||||
* @param string $charAt
|
||||
*/
|
||||
private function updateKeys($charAt)
|
||||
{
|
||||
$this->keys[0] = self::crc32($this->keys[0], $charAt);
|
||||
$this->keys[1] = ($this->keys[1] + ($this->keys[0] & 0xff)) & 4294967295;
|
||||
$this->keys[1] = ($this->keys[1] * 134775813 + 1) & 4294967295;
|
||||
$this->keys[2] = self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update crc.
|
||||
*
|
||||
* @param int $oldCrc
|
||||
* @param string $charAt
|
||||
* @return int
|
||||
*/
|
||||
private function crc32($oldCrc, $charAt)
|
||||
{
|
||||
return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
*/
|
||||
public function decrypt($content)
|
||||
{
|
||||
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
|
||||
foreach ($headerBytes as &$byte) {
|
||||
$byte = ($byte ^ $this->decryptByte()) & 0xff;
|
||||
$this->updateKeys($byte);
|
||||
}
|
||||
|
||||
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
// compare against the file type from extended local headers
|
||||
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
|
||||
} else {
|
||||
// compare against the CRC otherwise
|
||||
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
|
||||
}
|
||||
if ($headerBytes[11] !== $checkByte) {
|
||||
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName());
|
||||
}
|
||||
|
||||
$outputContent = "";
|
||||
foreach (unpack('C*', substr($content, self::STD_DEC_HDR_SIZE)) as $val) {
|
||||
$val = ($val ^ $this->decryptByte()) & 0xff;
|
||||
$this->updateKeys($val);
|
||||
$outputContent .= pack('c', $val);
|
||||
}
|
||||
return $outputContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt byte.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function decryptByte()
|
||||
{
|
||||
$temp = $this->keys[2] | 2;
|
||||
return (($temp * ($temp ^ 1)) >> 8) & 0xffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption data
|
||||
*
|
||||
* @param string $data
|
||||
* @param int $crc
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($data, $crc)
|
||||
{
|
||||
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
|
||||
|
||||
// Initialize again since the generated bytes were encrypted.
|
||||
$this->initKeys($this->entry->getPassword());
|
||||
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
|
||||
$headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
|
||||
|
||||
$headerBytes = $this->encryptData($headerBytes);
|
||||
return $headerBytes . $this->encryptData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
private function encryptData($content)
|
||||
{
|
||||
if ($content === null) {
|
||||
throw new \RuntimeException();
|
||||
}
|
||||
$buff = '';
|
||||
foreach (unpack('C*', $content) as $val) {
|
||||
$buff .= pack('c', $this->encryptByte($val));
|
||||
}
|
||||
return $buff;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $byte
|
||||
* @return int
|
||||
*/
|
||||
protected function encryptByte($byte)
|
||||
{
|
||||
$tempVal = $byte ^ $this->decryptByte() & 0xff;
|
||||
$this->updateKeys($byte);
|
||||
return $tempVal;
|
||||
}
|
||||
}
|
231
src/PhpZip/Crypto/WinZipAesEngine.php
Normal file
231
src/PhpZip/Crypto/WinZipAesEngine.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?php
|
||||
namespace PhpZip\Crypto;
|
||||
|
||||
use PhpZip\Exception\ZipAuthenticationException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Util\CryptoUtil;
|
||||
|
||||
/**
|
||||
* WinZip Aes Encryption Engine.
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class WinZipAesEngine
|
||||
{
|
||||
/**
|
||||
* The block size of the Advanced Encryption Specification (AES) Algorithm
|
||||
* in bits (AES_BLOCK_SIZE_BITS).
|
||||
*/
|
||||
const AES_BLOCK_SIZE_BITS = 128;
|
||||
const PWD_VERIFIER_BITS = 16;
|
||||
/**
|
||||
* The iteration count for the derived keys of the cipher, KLAC and MAC.
|
||||
*/
|
||||
const ITERATION_COUNT = 1000;
|
||||
/**
|
||||
* @var ZipEntry
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* WinZipAesEngine constructor.
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt from stream resource.
|
||||
*
|
||||
* @param resource $stream Input stream resource
|
||||
* @return string
|
||||
* @throws ZipAuthenticationException
|
||||
* @throws ZipCryptoException
|
||||
*/
|
||||
public function decrypt($stream)
|
||||
{
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
if (null === $field) {
|
||||
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
|
||||
}
|
||||
|
||||
$pos = ftell($stream);
|
||||
|
||||
// Get key strength.
|
||||
$keyStrengthBits = $field->getKeyStrength();
|
||||
$keyStrengthBytes = $keyStrengthBits / 8;
|
||||
|
||||
$salt = fread($stream, $keyStrengthBytes / 2);
|
||||
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8);
|
||||
|
||||
$sha1Size = 20;
|
||||
|
||||
// Init start, end and size of encrypted data.
|
||||
$endPos = $pos + $this->entry->getCompressedSize();
|
||||
$start = ftell($stream);
|
||||
$footerSize = $sha1Size / 2;
|
||||
$end = $endPos - $footerSize;
|
||||
$size = $end - $start;
|
||||
|
||||
if (0 > $size) {
|
||||
throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)");
|
||||
}
|
||||
|
||||
// Load authentication code.
|
||||
fseek($stream, $end, SEEK_SET);
|
||||
$authenticationCode = fread($stream, $footerSize);
|
||||
if (ftell($stream) !== $endPos) {
|
||||
// This should never happen unless someone is writing to the
|
||||
// end of the file concurrently!
|
||||
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
|
||||
}
|
||||
|
||||
do {
|
||||
assert($this->entry->getPassword() !== null);
|
||||
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
|
||||
|
||||
// Here comes the strange part about WinZip AES encryption:
|
||||
// Its unorthodox use of the Password-Based Key Derivation
|
||||
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
|
||||
// Yes, the password verifier is only a 16 bit value.
|
||||
// So we must use the MAC for password verification, too.
|
||||
$keyParam = hash_pbkdf2("sha1", $this->entry->getPassword(), $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
|
||||
$key = substr($keyParam, 0, $keyStrengthBytes);
|
||||
|
||||
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
|
||||
// Verify password.
|
||||
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
|
||||
|
||||
$content = stream_get_contents($stream, $size, $start);
|
||||
$mac = hash_hmac('sha1', $content, $sha1MacParam, true);
|
||||
|
||||
if ($authenticationCode !== substr($mac, 0, 10)) {
|
||||
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)");
|
||||
}
|
||||
|
||||
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decryption or encryption AES-CTR with Segment Integer Count (SIC).
|
||||
*
|
||||
* @param bool $encrypted If true encryption else decryption
|
||||
* @param string $str Data
|
||||
* @param string $key Key
|
||||
* @param string $iv IV
|
||||
* @return string
|
||||
*/
|
||||
private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv)
|
||||
{
|
||||
$numOfBlocks = ceil(strlen($str) / 16);
|
||||
$ctrStr = '';
|
||||
for ($i = 0; $i < $numOfBlocks; ++$i) {
|
||||
for ($j = 0; $j < 16; ++$j) {
|
||||
$n = ord($iv[$j]);
|
||||
if (++$n === 0x100) {
|
||||
// overflow, set this one to 0, increment next
|
||||
$iv[$j] = chr(0);
|
||||
} else {
|
||||
// no overflow, just write incremented number back and abort
|
||||
$iv[$j] = chr($n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data = substr($str, $i * 16, 16);
|
||||
$ctrStr .= $encrypted ?
|
||||
self::encryptCtr($data, $key, $iv) :
|
||||
self::decryptCtr($data, $key, $iv);
|
||||
}
|
||||
return $ctrStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt AES-CTR.
|
||||
*
|
||||
* @param string $data Raw data
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Encrypted data
|
||||
*/
|
||||
private static function encryptCtr($data, $key, $iv)
|
||||
{
|
||||
if (extension_loaded("openssl")) {
|
||||
$numBits = strlen($key) * 8;
|
||||
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt AES-CTR.
|
||||
*
|
||||
* @param string $data Encrypted data
|
||||
* @param string $key Aes key
|
||||
* @param string $iv Aes IV
|
||||
* @return string Raw data
|
||||
*/
|
||||
private static function decryptCtr($data, $key, $iv)
|
||||
{
|
||||
if (extension_loaded("openssl")) {
|
||||
$numBits = strlen($key) * 8;
|
||||
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
|
||||
} elseif (extension_loaded("mcrypt")) {
|
||||
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
|
||||
} else {
|
||||
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption string.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($content)
|
||||
{
|
||||
// Init key strength.
|
||||
$password = $this->entry->getPassword();
|
||||
assert($password !== null);
|
||||
|
||||
$keyStrengthBytes = 32;
|
||||
$keyStrengthBits = $keyStrengthBytes * 8;
|
||||
|
||||
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
|
||||
|
||||
$salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
|
||||
|
||||
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
|
||||
$sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
|
||||
|
||||
// Can you believe they "forgot" the nonce in the CTR mode IV?! :-(
|
||||
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
|
||||
$iv = str_repeat(chr(0), $ctrIvSize);
|
||||
|
||||
$key = substr($keyParam, 0, $keyStrengthBytes);
|
||||
|
||||
$content = self::aesCtrSegmentIntegerCounter(true, $content, $key, $iv);
|
||||
|
||||
$mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
|
||||
|
||||
return ($salt .
|
||||
substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) .
|
||||
$content .
|
||||
substr($mac, 0, 10)
|
||||
);
|
||||
}
|
||||
}
|
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
384
src/PhpZip/Model/ZipInfo.php
Normal file
384
src/PhpZip/Model/ZipInfo.php
Normal file
@ -0,0 +1,384 @@
|
||||
<?php
|
||||
namespace PhpZip\Model;
|
||||
|
||||
use PhpZip\Extra\NtfsExtraField;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Util\FilesUtil;
|
||||
|
||||
/**
|
||||
* Zip info
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipInfo
|
||||
{
|
||||
// made by constants
|
||||
const MADE_BY_MS_DOS = 0;
|
||||
const MADE_BY_AMIGA = 1;
|
||||
const MADE_BY_OPEN_VMS = 2;
|
||||
const MADE_BY_UNIX = 3;
|
||||
const MADE_BY_VM_CMS = 4;
|
||||
const MADE_BY_ATARI = 5;
|
||||
const MADE_BY_OS_2 = 6;
|
||||
const MADE_BY_MACINTOSH = 7;
|
||||
const MADE_BY_Z_SYSTEM = 8;
|
||||
const MADE_BY_CP_M = 9;
|
||||
const MADE_BY_WINDOWS_NTFS = 10;
|
||||
const MADE_BY_MVS = 11;
|
||||
const MADE_BY_VSE = 12;
|
||||
const MADE_BY_ACORN_RISC = 13;
|
||||
const MADE_BY_VFAT = 14;
|
||||
const MADE_BY_ALTERNATE_MVS = 15;
|
||||
const MADE_BY_BEOS = 16;
|
||||
const MADE_BY_TANDEM = 17;
|
||||
const MADE_BY_OS_400 = 18;
|
||||
const MADE_BY_OS_X = 19;
|
||||
const MADE_BY_UNKNOWN = 20;
|
||||
|
||||
private static $valuesMadeBy = [
|
||||
self::MADE_BY_MS_DOS => 'FAT',
|
||||
self::MADE_BY_AMIGA => 'Amiga',
|
||||
self::MADE_BY_OPEN_VMS => 'OpenVMS',
|
||||
self::MADE_BY_UNIX => 'UNIX',
|
||||
self::MADE_BY_VM_CMS => 'VM/CMS',
|
||||
self::MADE_BY_ATARI => 'Atari ST',
|
||||
self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.',
|
||||
self::MADE_BY_MACINTOSH => 'Macintosh',
|
||||
self::MADE_BY_Z_SYSTEM => 'Z-System',
|
||||
self::MADE_BY_CP_M => 'CP/M',
|
||||
self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS',
|
||||
self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)',
|
||||
self::MADE_BY_VSE => 'VSE',
|
||||
self::MADE_BY_ACORN_RISC => 'Acorn Risc',
|
||||
self::MADE_BY_VFAT => 'VFAT',
|
||||
self::MADE_BY_ALTERNATE_MVS => 'Alternate MVS',
|
||||
self::MADE_BY_BEOS => 'BeOS',
|
||||
self::MADE_BY_TANDEM => 'Tandem',
|
||||
self::MADE_BY_OS_400 => 'OS/400',
|
||||
self::MADE_BY_OS_X => 'Mac OS X',
|
||||
];
|
||||
|
||||
private static $valuesCompressionMethod = [
|
||||
ZipEntry::METHOD_STORED => 'no compression',
|
||||
1 => 'shrink',
|
||||
2 => 'reduce level 1',
|
||||
3 => 'reduce level 2',
|
||||
4 => 'reduce level 3',
|
||||
5 => 'reduce level 4',
|
||||
6 => 'implode',
|
||||
7 => 'reserved for Tokenizing compression algorithm',
|
||||
ZipEntry::METHOD_DEFLATED => 'deflate',
|
||||
9 => 'deflate64',
|
||||
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
|
||||
11 => 'reserved by PKWARE',
|
||||
12 => 'bzip2',
|
||||
13 => 'reserved by PKWARE',
|
||||
14 => 'LZMA (EFS)',
|
||||
15 => 'reserved by PKWARE',
|
||||
16 => 'reserved by PKWARE',
|
||||
17 => 'reserved by PKWARE',
|
||||
18 => 'IBM TERSE',
|
||||
19 => 'IBM LZ77 z Architecture (PFS)',
|
||||
97 => 'WavPack',
|
||||
98 => 'PPMd version I, Rev 1',
|
||||
ZipEntry::WINZIP_AES => 'WinZip AES',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $folder;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $size;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $compressedSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $mtime;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $ctime;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $atime;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $encrypted;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $crc;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $method;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $platform;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $version;
|
||||
|
||||
/**
|
||||
* ZipInfo constructor.
|
||||
*
|
||||
* @param ZipEntry $entry
|
||||
*/
|
||||
public function __construct(ZipEntry $entry)
|
||||
{
|
||||
$mtime = $entry->getTime();
|
||||
$atime = null;
|
||||
$ctime = null;
|
||||
|
||||
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
|
||||
if ($field !== null && $field instanceof NtfsExtraField) {
|
||||
/**
|
||||
* @var NtfsExtraField $field
|
||||
*/
|
||||
$atime = $field->getAtime();
|
||||
$ctime = $field->getCtime();
|
||||
}
|
||||
|
||||
$this->path = $entry->getName();
|
||||
$this->folder = $entry->isDirectory();
|
||||
$this->size = $entry->getSize();
|
||||
$this->compressedSize = $entry->getCompressedSize();
|
||||
$this->mtime = $mtime;
|
||||
$this->ctime = $ctime;
|
||||
$this->atime = $atime;
|
||||
$this->encrypted = $entry->isEncrypted();
|
||||
$this->comment = $entry->getComment();
|
||||
$this->crc = $entry->getCrc();
|
||||
$this->method = self::getMethodName($entry);
|
||||
$this->platform = self::getPlatformName($entry);
|
||||
$this->version = $entry->getVersionNeededToExtract();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
* @return string
|
||||
*/
|
||||
public static function getMethodName(ZipEntry $entry)
|
||||
{
|
||||
$return = '';
|
||||
if ($entry->isEncrypted()) {
|
||||
if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
|
||||
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
if ($field !== null) {
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$return .= '-' . $field->getKeyStrength();
|
||||
if (isset(self::$valuesCompressionMethod[$field->getMethod()])) {
|
||||
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$field->getMethod()]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$return .= 'ZipCrypto';
|
||||
if (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
|
||||
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
}
|
||||
}
|
||||
} elseif (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
|
||||
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
|
||||
} else {
|
||||
$return = 'unknown';
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ZipEntry $entry
|
||||
* @return string
|
||||
*/
|
||||
public static function getPlatformName(ZipEntry $entry)
|
||||
{
|
||||
if (isset(self::$valuesMadeBy[$entry->getPlatform()])) {
|
||||
return self::$valuesMadeBy[$entry->getPlatform()];
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'path' => $this->getPath(),
|
||||
'folder' => $this->isFolder(),
|
||||
'size' => $this->getSize(),
|
||||
'compressed_size' => $this->getCompressedSize(),
|
||||
'modified' => $this->getMtime(),
|
||||
'created' => $this->getCtime(),
|
||||
'accessed' => $this->getAtime(),
|
||||
'encrypted' => $this->isEncrypted(),
|
||||
'comment' => $this->getComment(),
|
||||
'crc' => $this->getCrc(),
|
||||
'method' => $this->getMethod(),
|
||||
'platform' => $this->getPlatform(),
|
||||
'version' => $this->getVersion()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isFolder()
|
||||
{
|
||||
return $this->folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCompressedSize()
|
||||
{
|
||||
return $this->compressedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMtime()
|
||||
{
|
||||
return $this->mtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCtime()
|
||||
{
|
||||
return $this->ctime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getAtime()
|
||||
{
|
||||
return $this->atime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEncrypted()
|
||||
{
|
||||
return $this->encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCrc()
|
||||
{
|
||||
return $this->crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPlatform()
|
||||
{
|
||||
return $this->platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
function __toString()
|
||||
{
|
||||
return 'ZipInfo {'
|
||||
. 'Path="' . $this->getPath() . '", '
|
||||
. ($this->isFolder() ? 'Folder, ' : '')
|
||||
. 'Size=' . FilesUtil::humanSize($this->getSize())
|
||||
. ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize())
|
||||
. ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', '
|
||||
. ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '')
|
||||
. ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '')
|
||||
. ($this->isEncrypted() ? 'Encrypted, ' : '')
|
||||
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
|
||||
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
|
||||
. 'Method="' . $this->getMethod() . '", '
|
||||
. 'Platform="' . $this->getPlatform() . '", '
|
||||
. 'Version=' . $this->getVersion()
|
||||
. '}';
|
||||
}
|
||||
|
||||
|
||||
}
|
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);
|
||||
}
|
||||
}
|
44
src/PhpZip/Util/PackUtil.php
Normal file
44
src/PhpZip/Util/PackUtil.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace PhpZip\Util;
|
||||
|
||||
use PhpZip\Exception\ZipException;
|
||||
|
||||
/**
|
||||
* Pack util
|
||||
*
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class PackUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* @param int|string $longValue
|
||||
* @return string
|
||||
*/
|
||||
public static function packLongLE($longValue)
|
||||
{
|
||||
// TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 0) {return pack("P", $longValue);}
|
||||
|
||||
$left = 0xffffffff00000000;
|
||||
$right = 0x00000000ffffffff;
|
||||
|
||||
$r = ($longValue & $left) >> 32;
|
||||
$l = $longValue & $right;
|
||||
|
||||
return pack('VV', $l, $r);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $value
|
||||
* @return int
|
||||
* @throws ZipException
|
||||
*/
|
||||
public static function unpackLongLE($value)
|
||||
{
|
||||
// TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 0){ return current(unpack('P', $value)); }
|
||||
$unpack = unpack('Va/Vb', $value);
|
||||
return $unpack['a'] + ($unpack['b'] << 32);
|
||||
}
|
||||
|
||||
}
|
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;
|
||||
|
||||
}
|
908
src/PhpZip/ZipFile.php
Normal file
908
src/PhpZip/ZipFile.php
Normal file
@ -0,0 +1,908 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
|
||||
use PhpZip\Crypto\WinZipAesEngine;
|
||||
use PhpZip\Exception\Crc32Exception;
|
||||
use PhpZip\Exception\IllegalArgumentException;
|
||||
use PhpZip\Exception\ZipCryptoException;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\Exception\ZipNotFoundEntry;
|
||||
use PhpZip\Exception\ZipUnsupportMethod;
|
||||
use PhpZip\Extra\WinZipAesEntryExtraField;
|
||||
use PhpZip\Mapper\OffsetPositionMapper;
|
||||
use PhpZip\Mapper\PositionMapper;
|
||||
use PhpZip\Model\ZipEntry;
|
||||
use PhpZip\Model\ZipInfo;
|
||||
use PhpZip\Util\PackUtil;
|
||||
|
||||
/**
|
||||
* This class is able to open the .ZIP file in read mode and extract files from it.
|
||||
*
|
||||
* Implemented support traditional PKWARE encryption and WinZip AES encryption.
|
||||
* Implemented support ZIP64.
|
||||
* Implemented support skip a preamble like the one found in self extracting archives.
|
||||
*
|
||||
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
|
||||
* @author Ne-Lexa alexey@nelexa.ru
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
|
||||
{
|
||||
/**
|
||||
* Input seekable stream resource.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $inputStream;
|
||||
|
||||
/**
|
||||
* The total number of bytes in the ZIP archive.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $length;
|
||||
|
||||
/**
|
||||
* The charset to use for entry names and comments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $charset;
|
||||
|
||||
/**
|
||||
* The number of bytes in the preamble of this ZIP file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $preamble;
|
||||
|
||||
/**
|
||||
* The number of bytes in the postamble of this ZIP file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $postamble;
|
||||
|
||||
/**
|
||||
* Maps entry names to zip entries.
|
||||
*
|
||||
* @var ZipEntry[]
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* The file comment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* Maps offsets specified in the ZIP file to real offsets in the file.
|
||||
*
|
||||
* @var PositionMapper
|
||||
*/
|
||||
private $mapper;
|
||||
|
||||
/**
|
||||
* Private ZipFile constructor.
|
||||
*
|
||||
* @see ZipFile::openFromFile()
|
||||
* @see ZipFile::openFromString()
|
||||
* @see ZipFile::openFromStream()
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->mapper = new PositionMapper();
|
||||
$this->charset = "UTF-8";
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from file
|
||||
*
|
||||
* @param string $filename
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException if file doesn't exists.
|
||||
* @throws ZipException if can't open file.
|
||||
*/
|
||||
public static function openFromFile($filename)
|
||||
{
|
||||
if (!file_exists($filename)) {
|
||||
throw new IllegalArgumentException("File $filename can't exists.");
|
||||
}
|
||||
if (!($handle = fopen($filename, 'rb'))) {
|
||||
throw new ZipException("File $filename can't open.");
|
||||
}
|
||||
$zipFile = self::openFromStream($handle);
|
||||
$zipFile->length = filesize($filename);
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from stream resource
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException Invalid stream resource
|
||||
* or resource cannot seekable stream
|
||||
*/
|
||||
public static function openFromStream($handle)
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
throw new IllegalArgumentException("Invalid stream resource.");
|
||||
}
|
||||
$meta = stream_get_meta_data($handle);
|
||||
if (!$meta['seekable']) {
|
||||
throw new IllegalArgumentException("Resource cannot seekable stream.");
|
||||
}
|
||||
$zipFile = new self();
|
||||
$stats = fstat($handle);
|
||||
if (isset($stats['size'])) {
|
||||
$zipFile->length = $stats['size'];
|
||||
}
|
||||
$zipFile->checkZipFileSignature($handle);
|
||||
$numEntries = $zipFile->findCentralDirectory($handle);
|
||||
$zipFile->mountCentralDirectory($handle, $numEntries);
|
||||
if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) {
|
||||
assert(0 === $numEntries);
|
||||
$zipFile->checkZipFileSignature($handle);
|
||||
}
|
||||
assert(null !== $handle);
|
||||
assert(null !== $zipFile->charset);
|
||||
assert(null !== $zipFile->entries);
|
||||
assert(null !== $zipFile->mapper);
|
||||
$zipFile->inputStream = $handle;
|
||||
// Do NOT close stream!
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check zip file signature
|
||||
*
|
||||
* @param resource $handle
|
||||
* @throws ZipException if this not .ZIP file.
|
||||
*/
|
||||
private function checkZipFileSignature($handle)
|
||||
{
|
||||
rewind($handle);
|
||||
$signature = current(unpack('V', fread($handle, 4)));
|
||||
// Constraint: A ZIP file must start with a Local File Header
|
||||
// or a (ZIP64) End Of Central Directory Record if it's empty.
|
||||
if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
|
||||
) {
|
||||
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the file pointer at the first Central File Header.
|
||||
* Performs some means to check that this is really a ZIP file.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return int
|
||||
* @throws ZipException If the file is not compatible to the ZIP File
|
||||
* Format Specification.
|
||||
*/
|
||||
private function findCentralDirectory($handle)
|
||||
{
|
||||
// Search for End of central directory record.
|
||||
$max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
|
||||
$min = $max >= 0xffff ? $max - 0xffff : 0;
|
||||
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
|
||||
fseek($handle, $endOfCentralDirRecordPos, SEEK_SET);
|
||||
// end of central dir signature 4 bytes (0x06054b50)
|
||||
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4))))
|
||||
continue;
|
||||
|
||||
// Process End Of Central Directory Record.
|
||||
$data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4);
|
||||
|
||||
/**
|
||||
* @var int $diskNo number of this disk - 2 bytes
|
||||
* @var int $cdDiskNo number of the disk with the start of the
|
||||
* central directory - 2 bytes
|
||||
* @var int $cdEntriesDisk total number of entries in the central
|
||||
* directory on this disk - 2 bytes
|
||||
* @var int $cdEntries total number of entries in the central
|
||||
* directory - 2 bytes
|
||||
* @var int $cdSize size of the central directory - 4 bytes
|
||||
* @var int $cdPos offset of start of central directory with
|
||||
* respect to the starting disk number - 4 bytes
|
||||
* @var int $commentLen ZIP file comment length - 2 bytes
|
||||
*/
|
||||
$unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data);
|
||||
extract($unpack);
|
||||
|
||||
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
|
||||
throw new ZipException(
|
||||
"ZIP file spanning/splitting is not supported!"
|
||||
);
|
||||
}
|
||||
// .ZIP file comment (variable size)
|
||||
if (0 < $commentLen) {
|
||||
$this->comment = fread($handle, $commentLen);
|
||||
}
|
||||
$this->preamble = $endOfCentralDirRecordPos;
|
||||
$this->postamble = $this->length - ftell($handle);
|
||||
|
||||
// Check for ZIP64 End Of Central Directory Locator.
|
||||
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
|
||||
|
||||
fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET);
|
||||
|
||||
// zip64 end of central dir locator
|
||||
// signature 4 bytes (0x07064b50)
|
||||
if (
|
||||
0 > $endOfCentralDirLocatorPos ||
|
||||
ftell($handle) === $this->length ||
|
||||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4)))
|
||||
) {
|
||||
// Seek and check first CFH, probably requiring an offset mapper.
|
||||
$offset = $endOfCentralDirRecordPos - $cdSize;
|
||||
fseek($handle, $offset, SEEK_SET);
|
||||
$offset -= $cdPos;
|
||||
if (0 !== $offset) {
|
||||
$this->mapper = new OffsetPositionMapper($offset);
|
||||
}
|
||||
return (int)$cdEntries;
|
||||
}
|
||||
|
||||
// number of the disk with the
|
||||
// start of the zip64 end of
|
||||
// central directory 4 bytes
|
||||
$zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4)));
|
||||
// relative offset of the zip64
|
||||
// end of central directory record 8 bytes
|
||||
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// total number of disks 4 bytes
|
||||
$totalDisks = current(unpack('V', fread($handle, 4)));
|
||||
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
|
||||
throw new ZipException("ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
|
||||
// zip64 end of central dir
|
||||
// signature 4 bytes (0x06064b50)
|
||||
$zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4)));
|
||||
if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
|
||||
throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
|
||||
}
|
||||
// size of zip64 end of central
|
||||
// directory record 8 bytes
|
||||
// version made by 2 bytes
|
||||
// version needed to extract 2 bytes
|
||||
fseek($handle, 12, SEEK_CUR);
|
||||
// number of this disk 4 bytes
|
||||
$diskNo = current(unpack('V', fread($handle, 4)));
|
||||
// number of the disk with the
|
||||
// start of the central directory 4 bytes
|
||||
$cdDiskNo = current(unpack('V', fread($handle, 4)));
|
||||
// total number of entries in the
|
||||
// central directory on this disk 8 bytes
|
||||
$cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// total number of entries in the
|
||||
// central directory 8 bytes
|
||||
$cdEntries = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
|
||||
throw new ZipException(
|
||||
"ZIP file spanning/splitting is not supported!");
|
||||
}
|
||||
if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
|
||||
throw new ZipException(
|
||||
"Total Number Of Entries In The Central Directory out of range!");
|
||||
}
|
||||
// size of the central directory 8 bytes
|
||||
//$cdSize = self::getLongLE($channel);
|
||||
fseek($handle, 8, SEEK_CUR);
|
||||
// offset of start of central
|
||||
// directory with respect to
|
||||
// the starting disk number 8 bytes
|
||||
$cdPos = PackUtil::unpackLongLE(fread($handle, 8));
|
||||
// zip64 extensible data sector (variable size)
|
||||
fseek($handle, $cdPos, SEEK_SET);
|
||||
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
|
||||
return (int)$cdEntries;
|
||||
}
|
||||
// Start recovering file entries from min.
|
||||
$this->preamble = $min;
|
||||
$this->postamble = $this->length - $min;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the central directory from the given seekable byte channel
|
||||
* and populates the internal tables with ZipEntry instances.
|
||||
*
|
||||
* The ZipEntry's will know all data that can be obtained from the
|
||||
* central directory alone, but not the data that requires the local
|
||||
* file header or additional data to be read.
|
||||
*
|
||||
* @param resource $handle Input channel.
|
||||
* @param int $numEntries Size zip entries.
|
||||
* @throws ZipException
|
||||
*/
|
||||
private function mountCentralDirectory($handle, $numEntries)
|
||||
{
|
||||
$numEntries = (int)$numEntries;
|
||||
$entries = [];
|
||||
for (; ; $numEntries--) {
|
||||
// central file header signature 4 bytes (0x02014b50)
|
||||
if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) {
|
||||
break;
|
||||
}
|
||||
// version made by 2 bytes
|
||||
$versionMadeBy = current(unpack('v', fread($handle, 2)));
|
||||
|
||||
// version needed to extract 2 bytes
|
||||
fseek($handle, 2, SEEK_CUR);
|
||||
|
||||
$unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26));
|
||||
|
||||
// disk number start 2 bytes
|
||||
// internal file attributes 2 bytes
|
||||
fseek($handle, 4, SEEK_CUR);
|
||||
|
||||
// external file attributes 4 bytes
|
||||
// relative offset of local header 4 bytes
|
||||
$unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8));
|
||||
|
||||
$utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8);
|
||||
if ($utf8) {
|
||||
$this->charset = "UTF-8";
|
||||
}
|
||||
|
||||
// See appendix D of PKWARE's ZIP File Format Specification.
|
||||
$name = fread($handle, $unpack['fileLen']);
|
||||
$entry = new ZipEntry($name, $handle);
|
||||
$entry->setRawPlatform($versionMadeBy >> 8);
|
||||
$entry->setGeneralPurposeBitFlags($unpack['gpbf']);
|
||||
$entry->setRawMethod($unpack['rawMethod']);
|
||||
$entry->setRawTime($unpack['rawTime']);
|
||||
$entry->setRawCrc($unpack['rawCrc']);
|
||||
$entry->setRawCompressedSize($unpack['rawCompressedSize']);
|
||||
$entry->setRawSize($unpack['rawSize']);
|
||||
$entry->setRawExternalAttributes($unpack2['rawExternalAttributes']);
|
||||
$entry->setRawOffset($unpack2['lfhOff']); // must be unmapped!
|
||||
if (0 < $unpack['extraLen']) {
|
||||
$entry->setRawExtraFields(fread($handle, $unpack['extraLen']));
|
||||
}
|
||||
if (0 < $unpack['commentLen']) {
|
||||
$entry->setComment(fread($handle, $unpack['commentLen']));
|
||||
}
|
||||
|
||||
unset($unpack, $unpack2);
|
||||
|
||||
// Re-load virtual offset after ZIP64 Extended Information
|
||||
// Extra Field may have been parsed, map it to the real
|
||||
// offset and conditionally update the preamble size from it.
|
||||
$lfhOff = $this->mapper->map($entry->getOffset());
|
||||
if ($lfhOff < $this->preamble) {
|
||||
$this->preamble = $lfhOff;
|
||||
}
|
||||
$entries[$entry->getName()] = $entry;
|
||||
}
|
||||
|
||||
if (0 !== $numEntries % 0x10000) {
|
||||
throw new ZipException("Expected " . abs($numEntries) .
|
||||
($numEntries > 0 ? " more" : " less") .
|
||||
" entries in the Central Directory!");
|
||||
}
|
||||
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open zip archive from raw string data.
|
||||
*
|
||||
* @param string $data
|
||||
* @return ZipFile
|
||||
* @throws IllegalArgumentException if data not available.
|
||||
* @throws ZipException if can't open temp stream.
|
||||
*/
|
||||
public static function openFromString($data)
|
||||
{
|
||||
if (empty($data)) {
|
||||
throw new IllegalArgumentException("Data not available");
|
||||
}
|
||||
if (!($handle = fopen('php://temp', 'r+b'))) {
|
||||
throw new ZipException("Can't open temp stream.");
|
||||
}
|
||||
fwrite($handle, $data);
|
||||
rewind($handle);
|
||||
$zipFile = self::openFromStream($handle);
|
||||
$zipFile->length = strlen($data);
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in this ZIP file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return sizeof($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list files.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getListFiles()
|
||||
{
|
||||
return array_keys($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return ZipEntry[]
|
||||
*/
|
||||
public function getRawEntries()
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a entry exists
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasEntry($entryName)
|
||||
{
|
||||
return isset($this->entries[$entryName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the directory entry.
|
||||
* Returns true if and only if this ZIP entry represents a directory entry
|
||||
* (i.e. end with '/').
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return bool
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function isDirectory($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
return $this->entries[$entryName]->isDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password to all encrypted entries.
|
||||
*
|
||||
* @param string $password Password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
foreach ($this->entries as $entry) {
|
||||
if ($entry->isEncrypted()) {
|
||||
$entry->setPassword($password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password to concrete zip entry.
|
||||
*
|
||||
* @param string $entryName Zip entry name
|
||||
* @param string $password Password
|
||||
* @throws ZipNotFoundEntry if don't exist zip entry.
|
||||
*/
|
||||
public function setEntryPassword($entryName, $password)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
if ($entry->isEncrypted()) {
|
||||
$entry->setPassword($password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file comment.
|
||||
*
|
||||
* @return string The file comment.
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return null === $this->comment ? '' : $this->decode($this->comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode charset entry name.
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function decode($text)
|
||||
{
|
||||
$inCharset = mb_detect_encoding($text, mb_detect_order(), true);
|
||||
if ($inCharset === $this->charset) return $text;
|
||||
return iconv($inCharset, $this->charset, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entry comment.
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return string
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function getEntryComment($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry("Not found entry " . $entryName);
|
||||
}
|
||||
return $this->entries[$entryName]->getComment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the character set which is effectively used for
|
||||
* decoding entry names and the file comment.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file length of this ZIP file in bytes.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function length()
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info by entry.
|
||||
*
|
||||
* @param string|ZipEntry $entryName
|
||||
* @return ZipInfo
|
||||
* @throws ZipNotFoundEntry
|
||||
*/
|
||||
public function getEntryInfo($entryName)
|
||||
{
|
||||
if ($entryName instanceof ZipEntry) {
|
||||
$entryName = $entryName->getName();
|
||||
}
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
|
||||
return new ZipInfo($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info by all entries.
|
||||
*
|
||||
* @return ZipInfo[]
|
||||
*/
|
||||
public function getAllInfo()
|
||||
{
|
||||
return array_map([$this, 'getEntryInfo'], $this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the archive contents
|
||||
*
|
||||
* Extract the complete archive or the given files to the specified destination.
|
||||
*
|
||||
* @param string $destination Location where to extract the files.
|
||||
* @param array $entries The entries to extract. It accepts
|
||||
* either a single entry name or an array of names.
|
||||
* @return bool
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function extractTo($destination, $entries = null)
|
||||
{
|
||||
if ($this->entries === null) {
|
||||
throw new ZipException("Zip entries not initial");
|
||||
}
|
||||
if (!file_exists($destination)) {
|
||||
throw new ZipException("Destination " . $destination . " not found");
|
||||
}
|
||||
if (!is_dir($destination)) {
|
||||
throw new ZipException("Destination is not directory");
|
||||
}
|
||||
if (!is_writable($destination)) {
|
||||
throw new ZipException("Destination is not writable directory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @var ZipEntry[] $zipEntries
|
||||
*/
|
||||
if (!empty($entries)) {
|
||||
if (is_string($entries)) {
|
||||
$entries = (array)$entries;
|
||||
}
|
||||
if (is_array($entries)) {
|
||||
$flipEntries = array_flip($entries);
|
||||
$zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) {
|
||||
/**
|
||||
* @var ZipEntry $zipEntry
|
||||
*/
|
||||
return isset($flipEntries[$zipEntry->getName()]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$zipEntries = $this->entries;
|
||||
}
|
||||
|
||||
$extract = 0;
|
||||
foreach ($zipEntries AS $entry) {
|
||||
$file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
|
||||
if ($entry->isDirectory()) {
|
||||
if (!is_dir($file)) {
|
||||
if (!mkdir($file, 0755, true)) {
|
||||
throw new ZipException("Can not create dir " . $file);
|
||||
}
|
||||
chmod($file, 0755);
|
||||
touch($file, $entry->getTime());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$dir = dirname($file);
|
||||
if (!file_exists($dir)) {
|
||||
if (!mkdir($dir, 0755, true)) {
|
||||
throw new ZipException("Can not create dir " . $dir);
|
||||
}
|
||||
chmod($dir, 0755);
|
||||
touch($file, $entry->getTime());
|
||||
}
|
||||
if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) {
|
||||
return false;
|
||||
}
|
||||
touch($file, $entry->getTime());
|
||||
$extract++;
|
||||
}
|
||||
return $extract > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an string content of the given entry.
|
||||
*
|
||||
* @param string $entryName
|
||||
* @return string|null
|
||||
* @throws ZipException
|
||||
*/
|
||||
public function getEntryContent($entryName)
|
||||
{
|
||||
if (!isset($this->entries[$entryName])) {
|
||||
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
|
||||
}
|
||||
$entry = $this->entries[$entryName];
|
||||
|
||||
$pos = $entry->getOffset();
|
||||
assert(ZipEntry::UNKNOWN !== $pos);
|
||||
$startPos = $pos = $this->mapper->map($pos);
|
||||
fseek($this->inputStream, $pos, SEEK_SET);
|
||||
$localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4)));
|
||||
if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) {
|
||||
throw new ZipException($entry->getName() . " (expected Local File Header)");
|
||||
}
|
||||
fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET);
|
||||
$unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4));
|
||||
$pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen'];
|
||||
|
||||
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
|
||||
|
||||
$check = $entry->isEncrypted();
|
||||
$method = $entry->getMethod();
|
||||
|
||||
$password = $entry->getPassword();
|
||||
if ($entry->isEncrypted() && empty($password)) {
|
||||
throw new ZipException("Not set password");
|
||||
}
|
||||
// Strong Encryption Specification - WinZip AES
|
||||
if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) {
|
||||
fseek($this->inputStream, $pos, SEEK_SET);
|
||||
$winZipAesEngine = new WinZipAesEngine($entry);
|
||||
$content = $winZipAesEngine->decrypt($this->inputStream);
|
||||
// Disable redundant CRC-32 check.
|
||||
$check = false;
|
||||
|
||||
/**
|
||||
* @var WinZipAesEntryExtraField $field
|
||||
*/
|
||||
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
|
||||
$method = $field->getMethod();
|
||||
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES);
|
||||
} else {
|
||||
// Get raw entry content
|
||||
$content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos);
|
||||
|
||||
// Traditional PKWARE Decryption
|
||||
if ($entry->isEncrypted()) {
|
||||
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
|
||||
$content = $zipCryptoEngine->decrypt($content);
|
||||
|
||||
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
|
||||
}
|
||||
}
|
||||
if ($check) {
|
||||
// Check CRC32 in the Local File Header or Data Descriptor.
|
||||
$localCrc = null;
|
||||
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
|
||||
// The CRC32 is in the Data Descriptor after the compressed
|
||||
// size.
|
||||
// Note the Data Descriptor's Signature is optional:
|
||||
// All newer apps should write it (and so does TrueVFS),
|
||||
// but older apps might not.
|
||||
fseek($this->inputStream, $pos + $entry->getCompressedSize(), SEEK_SET);
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
}
|
||||
} else {
|
||||
fseek($this->inputStream, $startPos + 14, SEEK_SET);
|
||||
// The CRC32 in the Local File Header.
|
||||
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
|
||||
}
|
||||
if ($entry->getCrc() !== $localCrc) {
|
||||
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case ZipEntry::METHOD_STORED:
|
||||
break;
|
||||
case ZipEntry::METHOD_DEFLATED:
|
||||
$content = gzinflate($content);
|
||||
break;
|
||||
case ZipEntry::METHOD_BZIP2:
|
||||
if (!extension_loaded('bz2')) {
|
||||
throw new ZipException('Extension bzip2 not install');
|
||||
}
|
||||
$content = bzdecompress($content);
|
||||
break;
|
||||
default:
|
||||
throw new ZipUnsupportMethod($entry->getName()
|
||||
. " (compression method "
|
||||
. $method
|
||||
. " is not supported)");
|
||||
}
|
||||
if ($check) {
|
||||
$localCrc = crc32($content);
|
||||
if ($entry->getCrc() !== $localCrc) {
|
||||
if ($entry->isEncrypted()) {
|
||||
throw new ZipCryptoException("Wrong password");
|
||||
}
|
||||
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all resources
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close zip archive and release input stream.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->length = null;
|
||||
|
||||
if ($this->inputStream !== null) {
|
||||
fclose($this->inputStream);
|
||||
$this->inputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a offset exists
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
* @param string $entryName An offset to check for.
|
||||
* @return boolean true on success or false on failure.
|
||||
* The return value will be casted to boolean if non-boolean was returned.
|
||||
*/
|
||||
public function offsetExists($entryName)
|
||||
{
|
||||
return isset($this->entries[$entryName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||
* @param string $entryName The offset to retrieve.
|
||||
* @return string|null
|
||||
*/
|
||||
public function offsetGet($entryName)
|
||||
{
|
||||
return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||
* @param string $entryName The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @throws ZipUnsupportMethod
|
||||
*/
|
||||
public function offsetSet($entryName, $value)
|
||||
{
|
||||
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
* @param string $entryName The offset to unset.
|
||||
* @throws ZipUnsupportMethod
|
||||
*/
|
||||
public function offsetUnset($entryName)
|
||||
{
|
||||
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @return mixed Can return any type.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->offsetGet($this->key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
* @return void Any returned value is ignored.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
next($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return key($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @return boolean The return value will be casted to boolean and then evaluated.
|
||||
* Returns true on success or false on failure.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->offsetExists($this->key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
* @return void Any returned value is ignored.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->entries);
|
||||
}
|
||||
}
|
1400
src/PhpZip/ZipOutputFile.php
Normal file
1400
src/PhpZip/ZipOutputFile.php
Normal file
File diff suppressed because it is too large
Load Diff
1049
tests/PhpZip/ZipTest.php
Normal file
1049
tests/PhpZip/ZipTest.php
Normal file
File diff suppressed because it is too large
Load Diff
45
tests/PhpZip/ZipTestCase.php
Normal file
45
tests/PhpZip/ZipTestCase.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace PhpZip;
|
||||
|
||||
/**
|
||||
* PHPUnit test case and helper methods.
|
||||
*/
|
||||
class ZipTestCase extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* Assert correct zip archive.
|
||||
*
|
||||
* @param $filename
|
||||
*/
|
||||
public static function assertCorrectZipArchive($filename)
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR !== '\\' && `which zip`) {
|
||||
exec("zip -T " . escapeshellarg($filename), $output, $returnCode);
|
||||
|
||||
$output = implode(PHP_EOL, $output);
|
||||
|
||||
self::assertEquals($returnCode, 0);
|
||||
self::assertNotContains('zip error', $output);
|
||||
self::assertContains(' OK', $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);
|
||||
}
|
||||
|
||||
}
|
@ -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-----
|
Loading…
x
Reference in New Issue
Block a user