This commit is contained in:
mkm 2023-07-18 11:17:17 +08:00
parent bb27d1de83
commit 5191330ff0
469 changed files with 7472 additions and 12951 deletions

805
composer.lock generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
jJCfJRvjXskoiDaS

View File

@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php',

View File

@ -9,7 +9,7 @@ return array(
'think\\view\\driver\\' => array($vendorDir . '/topthink/think-view/src'),
'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'),
'think\\app\\' => array($vendorDir . '/topthink/think-multi-app/src'),
'think\\' => array($vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-template/src', $vendorDir . '/topthink/framework/src/think'),
'think\\' => array($vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-template/src'),
'com\\junziqian\\sdk\\' => array($vendorDir . '/ebaoquan/junziqian_sdk/src/com/junziqian/sdk'),
'clagiordano\\weblibs\\configmanager\\' => array($vendorDir . '/clagiordano/weblibs-configmanager/src'),
'app\\' => array($baseDir . '/app'),
@ -50,6 +50,7 @@ return array(
'OSS\\' => array($vendorDir . '/aliyuncs/oss-sdk-php/src/OSS'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),
'Nyholm\\Psr7Server\\' => array($vendorDir . '/nyholm/psr7-server/src'),
'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
'JmesPath\\' => array($vendorDir . '/mtdowling/jmespath.php/src'),

View File

@ -9,6 +9,7 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
@ -113,6 +114,7 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
),
'M' =>
array (
'MyCLabs\\Enum\\' => 13,
'Monolog\\' => 8,
'Matrix\\' => 7,
),
@ -160,10 +162,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
),
'think\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/think-helper/src',
1 => __DIR__ . '/..' . '/topthink/think-orm/src',
2 => __DIR__ . '/..' . '/topthink/think-template/src',
3 => __DIR__ . '/..' . '/topthink/framework/src/think',
0 => __DIR__ . '/..' . '/topthink/framework/src/think',
1 => __DIR__ . '/..' . '/topthink/think-helper/src',
2 => __DIR__ . '/..' . '/topthink/think-orm/src',
3 => __DIR__ . '/..' . '/topthink/think-template/src',
),
'com\\junziqian\\sdk\\' =>
array (
@ -326,6 +328,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
array (
0 => __DIR__ . '/..' . '/nyholm/psr7-server/src',
),
'MyCLabs\\Enum\\' =>
array (
0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
),
'Monolog\\' =>
array (
0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',

View File

@ -275,18 +275,24 @@
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.3.2",
"version_normalized": "3.3.2.0",
"version": "v3.3.1",
"version_normalized": "3.3.1.0",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8"
"reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8",
"reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8",
"shasum": ""
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/be85b3f05b46c39bbc0d95f6c071ddff669510fa",
"reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2|^8.0",
@ -301,7 +307,7 @@
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"time": "2022-09-10T18:51:20+00:00",
"time": "2022-01-18T15:43:28+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -327,7 +333,7 @@
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2"
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.1"
},
"funding": [
{
@ -748,36 +754,38 @@
},
{
"name": "guzzlehttp/promises",
"version": "2.0.0",
"version_normalized": "2.0.0.0",
"version": "1.5.3",
"version_normalized": "1.5.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6"
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"shasum": ""
"url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2.5 || ^8.0"
"php": ">=5.5"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"time": "2023-05-21T13:50:22+00:00",
"time": "2023-05-21T12:31:43+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"installation-source": "dist",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
@ -814,7 +822,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.0"
"source": "https://github.com/guzzle/promises/tree/1.5.3"
},
"funding": [
{
@ -1052,38 +1060,41 @@
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.0",
"version_normalized": "3.1.0.0",
"version": "2.4.0",
"version_normalized": "2.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1"
"reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"shasum": ""
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3",
"reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.1"
"myclabs/php-enum": "^1.5",
"php": "^8.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"friendsofphp/php-cs-fixer": "^3.9",
"guzzlehttp/guzzle": "^6.5.3 || ^7.2.0",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0",
"php-coveralls/php-coveralls": "^2.4",
"phpunit/phpunit": "^8.5.8 || ^9.4.2",
"vimeo/psalm": "^5.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"time": "2023-06-21T14:59:35+00:00",
"time": "2022-12-08T12:29:14+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -1120,7 +1131,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.0"
"source": "https://github.com/maennchen/ZipStream-PHP/tree/2.4.0"
},
"funding": [
{
@ -1434,6 +1445,75 @@
},
"install-path": "../mtdowling/jmespath.php"
},
{
"name": "myclabs/php-enum",
"version": "1.8.3",
"version_normalized": "1.8.3.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "b942d263c641ddb5190929ff840c68f78713e937"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937",
"reference": "b942d263c641ddb5190929ff840c68f78713e937",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-json": "*",
"php": "^7.3 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2"
},
"time": "2021-07-05T08:18:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"description": "PHP Enum implementation",
"homepage": "http://github.com/myclabs/php-enum",
"keywords": [
"enum"
],
"support": {
"issues": "https://github.com/myclabs/php-enum/issues",
"source": "https://github.com/myclabs/php-enum/tree/1.8.3"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
"type": "tidelift"
}
],
"install-path": "../myclabs/php-enum"
},
{
"name": "nyholm/psr7",
"version": "1.8.0",
@ -1680,18 +1760,24 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.0",
"version_normalized": "1.29.0.0",
"version": "1.28.0",
"version_normalized": "1.28.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"shasum": ""
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-ctype": "*",
@ -1708,7 +1794,7 @@
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"maennchen/zipstream-php": "^2.1",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
@ -1720,12 +1806,12 @@
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mitoteam/jpgraph": "^10.2.4",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
@ -1736,7 +1822,7 @@
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"time": "2023-06-14T22:48:31+00:00",
"time": "2023-02-25T12:24:49+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -1782,7 +1868,7 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.28.0"
},
"install-path": "../phpoffice/phpspreadsheet"
},
@ -1903,24 +1989,30 @@
},
{
"name": "psr/http-client",
"version": "1.0.2",
"version_normalized": "1.0.2.0",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"shasum": ""
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
"psr/http-message": "^1.0"
},
"time": "2023-04-10T20:12:12+00:00",
"time": "2020-06-29T06:28:15+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -1940,7 +2032,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
@ -1952,7 +2044,7 @@
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client/tree/1.0.2"
"source": "https://github.com/php-fig/http-client/tree/master"
},
"install-path": "../psr/http-client"
},
@ -2391,18 +2483,24 @@
},
{
"name": "rmccue/requests",
"version": "v2.0.7",
"version_normalized": "2.0.7.0",
"version": "v2.0.5",
"version_normalized": "2.0.5.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/Requests.git",
"reference": "e14a6f4e7438d3f8da3f2657759e6367b906ee23"
"reference": "b717f1d2f4ef7992ec0c127747ed8b7e170c2f49"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/Requests/zipball/e14a6f4e7438d3f8da3f2657759e6367b906ee23",
"reference": "e14a6f4e7438d3f8da3f2657759e6367b906ee23",
"shasum": ""
"url": "https://api.github.com/repos/WordPress/Requests/zipball/b717f1d2f4ef7992ec0c127747ed8b7e170c2f49",
"reference": "b717f1d2f4ef7992ec0c127747ed8b7e170c2f49",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-json": "*",
@ -2419,7 +2517,7 @@
"wp-coding-standards/wpcs": "^2.0",
"yoast/phpunit-polyfills": "^1.0.0"
},
"time": "2023-06-02T07:35:42+00:00",
"time": "2022-10-11T08:15:28+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -2663,27 +2761,33 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.3.0",
"version_normalized": "3.3.0.0",
"version": "v3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
"shasum": ""
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1"
"php": ">=8.0.2"
},
"time": "2023-05-23T14:45:45+00:00",
"time": "2022-01-02T09:55:41+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.4-dev"
"dev-main": "3.0-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -2713,7 +2817,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2"
},
"funding": [
{
@ -2733,23 +2837,28 @@
},
{
"name": "symfony/http-client",
"version": "v6.2.12",
"version_normalized": "6.2.12.0",
"version": "v6.0.20",
"version_normalized": "6.0.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "7ef37b91629937843e8f52a2da5587ff16d75d25"
"reference": "541c04560da1875f62c963c3aab6ea12a7314e11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/7ef37b91629937843e8f52a2da5587ff16d75d25",
"reference": "7ef37b91629937843e8f52a2da5587ff16d75d25",
"shasum": ""
"url": "https://api.github.com/repos/symfony/http-client/zipball/541c04560da1875f62c963c3aab6ea12a7314e11",
"reference": "541c04560da1875f62c963c3aab6ea12a7314e11",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1",
"php": ">=8.0.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/http-client-contracts": "^3",
"symfony/service-contracts": "^1.0|^2|^3"
},
@ -2767,14 +2876,13 @@
"guzzlehttp/promises": "^1.4",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"php-http/message-factory": "^1.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/http-kernel": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
"symfony/stopwatch": "^5.4|^6.0"
},
"time": "2023-06-24T11:48:11+00:00",
"time": "2023-01-30T15:41:07+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -2801,11 +2909,8 @@
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v6.2.12"
"source": "https://github.com/symfony/http-client/tree/v6.0.20"
},
"funding": [
{
@ -2825,27 +2930,36 @@
},
{
"name": "symfony/http-client-contracts",
"version": "v3.3.0",
"version_normalized": "3.3.0.0",
"version": "v3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "3b66325d0176b4ec826bffab57c9037d759c31fb"
"reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/3b66325d0176b4ec826bffab57c9037d759c31fb",
"reference": "3b66325d0176b4ec826bffab57c9037d759c31fb",
"shasum": ""
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4184b9b63af1edaf35b6a7974c6f1f9f33294129",
"reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1"
"php": ">=8.0.2"
},
"time": "2023-05-23T14:45:45+00:00",
"suggest": {
"symfony/http-client-implementation": ""
},
"time": "2022-04-12T16:11:42+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.4-dev"
"dev-main": "3.0-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -2856,10 +2970,7 @@
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -2886,7 +2997,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.3.0"
"source": "https://github.com/symfony/http-client-contracts/tree/v3.0.2"
},
"funding": [
{
@ -2906,21 +3017,27 @@
},
{
"name": "symfony/http-foundation",
"version": "v6.1.12",
"version_normalized": "6.1.12.0",
"version": "v6.0.20",
"version_normalized": "6.0.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "bd1bcfb0eba14de22c4d086c5023e608f37366ed"
"reference": "e16b2676a4b3b1fa12378a20b29c364feda2a8d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1bcfb0eba14de22c4d086c5023e608f37366ed",
"reference": "bd1bcfb0eba14de22c4d086c5023e608f37366ed",
"shasum": ""
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/e16b2676a4b3b1fa12378a20b29c364feda2a8d6",
"reference": "e16b2676a4b3b1fa12378a20b29c364feda2a8d6",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-mbstring": "~1.1"
},
@ -2936,7 +3053,7 @@
"suggest": {
"symfony/mime": "To use the file extension guesser"
},
"time": "2023-01-30T15:43:30+00:00",
"time": "2023-01-30T15:41:07+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -2964,7 +3081,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v6.1.12"
"source": "https://github.com/symfony/http-foundation/tree/v6.0.20"
},
"funding": [
{
@ -2984,21 +3101,27 @@
},
{
"name": "symfony/mime",
"version": "v6.3.0",
"version_normalized": "6.3.0.0",
"version": "v6.0.19",
"version_normalized": "6.0.19.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad"
"reference": "d7052547a0070cbeadd474e172b527a00d657301"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/7b5d2121858cd6efbed778abce9cfdd7ab1f62ad",
"reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad",
"shasum": ""
"url": "https://api.github.com/repos/symfony/mime/zipball/d7052547a0070cbeadd474e172b527a00d657301",
"reference": "d7052547a0070cbeadd474e172b527a00d657301",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1",
"php": ">=8.0.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
@ -3007,18 +3130,17 @@
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<5.4",
"symfony/serializer": "<6.2"
"symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/property-access": "^5.4|^6.0",
"symfony/property-info": "^5.4|^6.0",
"symfony/serializer": "^6.2"
"symfony/serializer": "^5.4.14|~6.0.14|^6.1.6"
},
"time": "2023-04-28T15:57:00+00:00",
"time": "2023-01-11T11:50:03+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -3050,7 +3172,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v6.3.0"
"source": "https://github.com/symfony/mime/tree/v6.0.19"
},
"funding": [
{
@ -4082,26 +4204,32 @@
},
{
"name": "symfony/var-exporter",
"version": "v6.3.0",
"version_normalized": "6.3.0.0",
"version": "v6.0.19",
"version_normalized": "6.0.19.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "db5416d04269f2827d8c54331ba4cfa42620d350"
"reference": "df56f53818c2d5d9f683f4ad2e365ba73a3b69d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/db5416d04269f2827d8c54331ba4cfa42620d350",
"reference": "db5416d04269f2827d8c54331ba4cfa42620d350",
"shasum": ""
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/df56f53818c2d5d9f683f4ad2e365ba73a3b69d2",
"reference": "df56f53818c2d5d9f683f4ad2e365ba73a3b69d2",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1"
"php": ">=8.0.2"
},
"require-dev": {
"symfony/var-dumper": "^5.4|^6.0"
},
"time": "2023-04-21T08:48:44+00:00",
"time": "2023-01-13T08:34:10+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -4134,12 +4262,10 @@
"export",
"hydrate",
"instantiate",
"lazy-loading",
"proxy",
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v6.3.0"
"source": "https://github.com/symfony/var-exporter/tree/v6.0.19"
},
"funding": [
{
@ -4732,18 +4858,24 @@
},
{
"name": "w7corp/easywechat",
"version": "6.12.3",
"version_normalized": "6.12.3.0",
"version": "6.8.0",
"version_normalized": "6.8.0.0",
"source": {
"type": "git",
"url": "https://github.com/w7corp/easywechat.git",
"reference": "10b9f31b61b4e5327c9e4457dffcf307de2deb6c"
"reference": "60f0b4ba2ac3144df1a2291193daa34beb949d26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/w7corp/easywechat/zipball/10b9f31b61b4e5327c9e4457dffcf307de2deb6c",
"reference": "10b9f31b61b4e5327c9e4457dffcf307de2deb6c",
"shasum": ""
"url": "https://api.github.com/repos/w7corp/easywechat/zipball/60f0b4ba2ac3144df1a2291193daa34beb949d26",
"reference": "60f0b4ba2ac3144df1a2291193daa34beb949d26",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-curl": "*",
@ -4752,10 +4884,10 @@
"ext-openssl": "*",
"ext-simplexml": "*",
"ext-sodium": "*",
"monolog/monolog": "^2.2|^3.0",
"monolog/monolog": "^2.2",
"nyholm/psr7": "^1.5",
"nyholm/psr7-server": "^1.0",
"overtrue/socialite": "^3.5.4|^4.0.1",
"overtrue/socialite": "^3.5|^4.0.1",
"php": ">=8.0.2",
"psr/http-client": "^1.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
@ -4777,7 +4909,7 @@
"phpunit/phpunit": "^9.5",
"symfony/var-dumper": "^5.2"
},
"time": "2023-07-05T14:14:28+00:00",
"time": "2022-09-25T13:05:18+00:00",
"type": "library",
"extra": {
"hooks": {
@ -4823,7 +4955,7 @@
],
"support": {
"issues": "https://github.com/w7corp/easywechat/issues",
"source": "https://github.com/w7corp/easywechat/tree/6.12.3"
"source": "https://github.com/w7corp/easywechat/tree/6.8.0"
},
"funding": [
{

View File

@ -3,7 +3,7 @@
'name' => 'topthink/think',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b55f77d5c3e94177ec3d1ce21fce629a756733b6',
'reference' => '942e2b120398de55e20eaf3f36f1dab679db2c41',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -47,9 +47,9 @@
'dev_requirement' => false,
),
'dragonmantank/cron-expression' => array(
'pretty_version' => 'v3.3.2',
'version' => '3.3.2.0',
'reference' => '782ca5968ab8b954773518e9e49a6f892a34b2a8',
'pretty_version' => 'v3.3.1',
'version' => '3.3.1.0',
'reference' => 'be85b3f05b46c39bbc0d95f6c071ddff669510fa',
'type' => 'library',
'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
'aliases' => array(),
@ -101,9 +101,9 @@
'dev_requirement' => false,
),
'guzzlehttp/promises' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'reference' => '3a494dc7dc1d7d12e511890177ae2d0e6c107da6',
'pretty_version' => '1.5.3',
'version' => '1.5.3.0',
'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(),
@ -128,9 +128,9 @@
'dev_requirement' => false,
),
'maennchen/zipstream-php' => array(
'pretty_version' => '3.1.0',
'version' => '3.1.0.0',
'reference' => 'b8174494eda667f7d13876b4a7bfef0f62a7c0d1',
'pretty_version' => '2.4.0',
'version' => '2.4.0.0',
'reference' => '3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3',
'type' => 'library',
'install_path' => __DIR__ . '/../maennchen/zipstream-php',
'aliases' => array(),
@ -178,6 +178,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'myclabs/php-enum' => array(
'pretty_version' => '1.8.3',
'version' => '1.8.3.0',
'reference' => 'b942d263c641ddb5190929ff840c68f78713e937',
'type' => 'library',
'install_path' => __DIR__ . '/../myclabs/php-enum',
'aliases' => array(),
'dev_requirement' => false,
),
'nyholm/psr7' => array(
'pretty_version' => '1.8.0',
'version' => '1.8.0.0',
@ -224,9 +233,9 @@
),
),
'phpoffice/phpspreadsheet' => array(
'pretty_version' => '1.29.0',
'version' => '1.29.0.0',
'reference' => 'fde2ccf55eaef7e86021ff1acce26479160a0fa0',
'pretty_version' => '1.28.0',
'version' => '1.28.0.0',
'reference' => '6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
'aliases' => array(),
@ -257,9 +266,9 @@
'dev_requirement' => false,
),
'psr/http-client' => array(
'pretty_version' => '1.0.2',
'version' => '1.0.2.0',
'reference' => '0955afe48220520692d2d09f7ab7e0f93ffd6a31',
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-client',
'aliases' => array(),
@ -359,9 +368,9 @@
'dev_requirement' => false,
),
'rmccue/requests' => array(
'pretty_version' => 'v2.0.7',
'version' => '2.0.7.0',
'reference' => 'e14a6f4e7438d3f8da3f2657759e6367b906ee23',
'pretty_version' => 'v2.0.5',
'version' => '2.0.5.0',
'reference' => 'b717f1d2f4ef7992ec0c127747ed8b7e170c2f49',
'type' => 'library',
'install_path' => __DIR__ . '/../rmccue/requests',
'aliases' => array(),
@ -392,27 +401,27 @@
),
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.3.0',
'version' => '3.3.0.0',
'reference' => '7c3aff79d10325257a001fcf92d991f24fc967cf',
'pretty_version' => 'v3.0.2',
'version' => '3.0.2.0',
'reference' => '26954b3d62a6c5fd0ea8a2a00c0353a14978d05c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-client' => array(
'pretty_version' => 'v6.2.12',
'version' => '6.2.12.0',
'reference' => '7ef37b91629937843e8f52a2da5587ff16d75d25',
'pretty_version' => 'v6.0.20',
'version' => '6.0.20.0',
'reference' => '541c04560da1875f62c963c3aab6ea12a7314e11',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-client',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-client-contracts' => array(
'pretty_version' => 'v3.3.0',
'version' => '3.3.0.0',
'reference' => '3b66325d0176b4ec826bffab57c9037d759c31fb',
'pretty_version' => 'v3.0.2',
'version' => '3.0.2.0',
'reference' => '4184b9b63af1edaf35b6a7974c6f1f9f33294129',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-client-contracts',
'aliases' => array(),
@ -425,18 +434,18 @@
),
),
'symfony/http-foundation' => array(
'pretty_version' => 'v6.1.12',
'version' => '6.1.12.0',
'reference' => 'bd1bcfb0eba14de22c4d086c5023e608f37366ed',
'pretty_version' => 'v6.0.20',
'version' => '6.0.20.0',
'reference' => 'e16b2676a4b3b1fa12378a20b29c364feda2a8d6',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-foundation',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/mime' => array(
'pretty_version' => 'v6.3.0',
'version' => '6.3.0.0',
'reference' => '7b5d2121858cd6efbed778abce9cfdd7ab1f62ad',
'pretty_version' => 'v6.0.19',
'version' => '6.0.19.0',
'reference' => 'd7052547a0070cbeadd474e172b527a00d657301',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/mime',
'aliases' => array(),
@ -542,9 +551,9 @@
'dev_requirement' => true,
),
'symfony/var-exporter' => array(
'pretty_version' => 'v6.3.0',
'version' => '6.3.0.0',
'reference' => 'db5416d04269f2827d8c54331ba4cfa42620d350',
'pretty_version' => 'v6.0.19',
'version' => '6.0.19.0',
'reference' => 'df56f53818c2d5d9f683f4ad2e365ba73a3b69d2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/var-exporter',
'aliases' => array(),
@ -589,7 +598,7 @@
'topthink/think' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b55f77d5c3e94177ec3d1ce21fce629a756733b6',
'reference' => '942e2b120398de55e20eaf3f36f1dab679db2c41',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -650,9 +659,9 @@
'dev_requirement' => false,
),
'w7corp/easywechat' => array(
'pretty_version' => '6.12.3',
'version' => '6.12.3.0',
'reference' => '10b9f31b61b4e5327c9e4457dffcf307de2deb6c',
'pretty_version' => '6.8.0',
'version' => '6.8.0.0',
'reference' => '60f0b4ba2ac3144df1a2291193daa34beb949d26',
'type' => 'library',
'install_path' => __DIR__ . '/../w7corp/easywechat',
'aliases' => array(),

View File

@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80002)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.';
}
if ($issues) {

View File

@ -1,16 +1,5 @@
# Change Log
## [3.3.2] - 2022-09-19
### Added
- N/A
### Changed
- Skip some daylight savings time tests for PHP 8.1 daylight savings time weirdness (#146)
### Fixed
- Changed string interpolations to work better with PHP 8.2 (#142)
## [3.3.1] - 2022-01-18
### Added

View File

@ -37,11 +37,5 @@
"scripts": {
"phpstan": "./vendor/bin/phpstan analyze",
"test": "phpunit"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"phpstan/extension-installer": true
}
}
}

View File

@ -49,7 +49,7 @@ class DayOfMonthField extends AbstractField
private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime
{
$tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}");
$target = DateTime::createFromFormat('Y-m-d', "${currentYear}-${currentMonth}-${tday}");
if ($target === false) {
return null;
@ -94,9 +94,9 @@ class DayOfMonthField extends AbstractField
}
// Check to see if this is the nearest weekday to a particular value
if ($wPosition = strpos($value, 'W')) {
if (strpos($value, 'W')) {
// Parse the target day
$targetDay = (int) substr($value, 0, $wPosition);
$targetDay = (int) substr($value, 0, strpos($value, 'W'));
// Find out if the current day is the nearest day of the week
$nearest = self::getNearestWeekday(
(int) $date->format('Y'),

View File

@ -68,8 +68,8 @@ class DayOfWeekField extends AbstractField
$lastDayOfMonth = (int) $date->format('t');
// Find out if this is the last specific weekday of the month
if ($lPosition = strpos($value, 'L')) {
$weekday = $this->convertLiterals(substr($value, 0, $lPosition));
if (strpos($value, 'L')) {
$weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L')));
$weekday %= 7;
$daysInMonth = (int) $date->format('t');

View File

@ -25,7 +25,7 @@ class HoursField extends AbstractField
/**
* @var array|null Transitions returned by DateTimeZone::getTransitions()
*/
protected $transitions = [];
protected $transitions = null;
/**
* @var int|null Timestamp of the start of the transitions range
@ -92,7 +92,7 @@ class HoursField extends AbstractField
$dtLimitStart->getTimestamp(),
$dtLimitEnd->getTimestamp()
);
if (empty($this->transitions)) {
if ($this->transitions === false) {
return null;
}
$this->transitionsStart = $dtLimitStart->getTimestamp();

View File

@ -1,36 +1,17 @@
# CHANGELOG
## 2.0.0 - TBC
### Added
- Added PHP 7 type hints
### Changed
- All previously non-final non-exception classes have been marked as soft-final
### Removed
- Dropped PHP < 7.2 support
- All functions in the `GuzzleHttp\Promise` namespace
## 1.5.3 - 2023-05-21
### Changed
- Removed remaining usage of deprecated functions
## 1.5.2 - 2022-08-07
### Changed
- Officially support PHP 8.2
## 1.5.1 - 2021-10-22
### Fixed
@ -38,7 +19,6 @@
- Revert "Call handler when waiting on fulfilled/rejected Promise"
- Fix pool memory leak when empty array of promises provided
## 1.5.0 - 2021-10-07
### Changed
@ -50,14 +30,12 @@
- Fix manually settle promises generated with `Utils::task`
## 1.4.1 - 2021-02-18
### Fixed
- Fixed `each_limit` skipping promises and failing
## 1.4.0 - 2020-09-30
### Added

View File

@ -29,21 +29,6 @@ for a general introduction to promises.
`GuzzleHttp\Promise\Coroutine::of()`.
## Installation
```shell
composer require guzzlehttp/promises
```
## Version Guidance
| Version | Status | PHP Version |
|---------|------------------------|--------------|
| 1.x | Bug and security fixes | >=5.5,<8.3 |
| 2.x | Latest | >=7.2.5,<8.3 |
## Quick Start
A *promise* represents the eventual result of an asynchronous operation. The
@ -445,6 +430,8 @@ $loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(0, [$queue, 'run']);
```
*TODO*: Perhaps adding a `futureTick()` on each tick would be faster?
## Implementation Notes
@ -514,8 +501,8 @@ $promise->resolve('foo');
A static API was first introduced in 1.4.0, in order to mitigate problems with
functions conflicting between global and local copies of the package. The
function API was removed in 2.0.0. A migration table has been provided here for
your convenience:
function API will be removed in 2.0.0. A migration table has been provided here
for your convenience:
| Original Function | Replacement Method |
|----------------|----------------|

View File

@ -26,32 +26,27 @@
}
],
"require": {
"php": "^7.2.5 || ^8.0"
"php": ">=5.5"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"files": ["src/functions_include.php"]
},
"autoload-dev": {
"psr-4": {
"GuzzleHttp\\Promise\\Tests\\": "tests/"
}
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
"scripts": {
"test": "vendor/bin/simple-phpunit",
"test-ci": "vendor/bin/simple-phpunit --coverage-text"
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist",
"sort-packages": true
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -9,7 +7,7 @@ namespace GuzzleHttp\Promise;
*/
class AggregateException extends RejectionException
{
public function __construct(string $msg, array $reasons)
public function __construct($msg, array $reasons)
{
parent::__construct(
$reasons,

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**

View File

@ -1,9 +1,8 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
use Exception;
use Generator;
use Throwable;
@ -28,7 +27,7 @@ use Throwable;
* $value = (yield createPromise('a'));
* try {
* $value = (yield createPromise($value . 'b'));
* } catch (\Throwable $e) {
* } catch (\Exception $e) {
* // The promise was rejected.
* }
* yield $value . 'c';
@ -41,7 +40,7 @@ use Throwable;
*
* @return Promise
*
* @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration
* @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration
*/
final class Coroutine implements PromiseInterface
{
@ -63,13 +62,15 @@ final class Coroutine implements PromiseInterface
public function __construct(callable $generatorFn)
{
$this->generator = $generatorFn();
$this->result = new Promise(function (): void {
$this->result = new Promise(function () {
while (isset($this->currentPromise)) {
$this->currentPromise->wait();
}
});
try {
$this->nextCoroutine($this->generator->current());
} catch (\Exception $exception) {
$this->result->reject($exception);
} catch (Throwable $throwable) {
$this->result->reject($throwable);
}
@ -77,8 +78,10 @@ final class Coroutine implements PromiseInterface
/**
* Create a new coroutine.
*
* @return self
*/
public static function of(callable $generatorFn): self
public static function of(callable $generatorFn)
{
return new self($generatorFn);
}
@ -86,42 +89,42 @@ final class Coroutine implements PromiseInterface
public function then(
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
return $this->result->then($onFulfilled, $onRejected);
}
public function otherwise(callable $onRejected): PromiseInterface
public function otherwise(callable $onRejected)
{
return $this->result->otherwise($onRejected);
}
public function wait(bool $unwrap = true)
public function wait($unwrap = true)
{
return $this->result->wait($unwrap);
}
public function getState(): string
public function getState()
{
return $this->result->getState();
}
public function resolve($value): void
public function resolve($value)
{
$this->result->resolve($value);
}
public function reject($reason): void
public function reject($reason)
{
$this->result->reject($reason);
}
public function cancel(): void
public function cancel()
{
$this->currentPromise->cancel();
$this->result->cancel();
}
private function nextCoroutine($yielded): void
private function nextCoroutine($yielded)
{
$this->currentPromise = Create::promiseFor($yielded)
->then([$this, '_handleSuccess'], [$this, '_handleFailure']);
@ -130,7 +133,7 @@ final class Coroutine implements PromiseInterface
/**
* @internal
*/
public function _handleSuccess($value): void
public function _handleSuccess($value)
{
unset($this->currentPromise);
try {
@ -140,6 +143,8 @@ final class Coroutine implements PromiseInterface
} else {
$this->result->resolve($value);
}
} catch (Exception $exception) {
$this->result->reject($exception);
} catch (Throwable $throwable) {
$this->result->reject($throwable);
}
@ -148,13 +153,15 @@ final class Coroutine implements PromiseInterface
/**
* @internal
*/
public function _handleFailure($reason): void
public function _handleFailure($reason)
{
unset($this->currentPromise);
try {
$nextYield = $this->generator->throw(Create::exceptionFor($reason));
// The throw was caught, so keep iterating on the coroutine
$this->nextCoroutine($nextYield);
} catch (Exception $exception) {
$this->result->reject($exception);
} catch (Throwable $throwable) {
$this->result->reject($throwable);
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
final class Create
@ -10,8 +8,10 @@ final class Create
* Creates a promise for a value if the value is not a promise.
*
* @param mixed $value Promise or value.
*
* @return PromiseInterface
*/
public static function promiseFor($value): PromiseInterface
public static function promiseFor($value)
{
if ($value instanceof PromiseInterface) {
return $value;
@ -23,7 +23,6 @@ final class Create
$cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null;
$promise = new Promise($wfn, $cfn);
$value->then([$promise, 'resolve'], [$promise, 'reject']);
return $promise;
}
@ -35,8 +34,10 @@ final class Create
* If the provided reason is a promise, then it is returned as-is.
*
* @param mixed $reason Promise or reason.
*
* @return PromiseInterface
*/
public static function rejectionFor($reason): PromiseInterface
public static function rejectionFor($reason)
{
if ($reason instanceof PromiseInterface) {
return $reason;
@ -49,10 +50,12 @@ final class Create
* Create an exception for a rejected promise value.
*
* @param mixed $reason
*
* @return \Exception|\Throwable
*/
public static function exceptionFor($reason): \Throwable
public static function exceptionFor($reason)
{
if ($reason instanceof \Throwable) {
if ($reason instanceof \Exception || $reason instanceof \Throwable) {
return $reason;
}
@ -63,8 +66,10 @@ final class Create
* Returns an iterator for the given value.
*
* @param mixed $value
*
* @return \Iterator
*/
public static function iterFor($value): \Iterator
public static function iterFor($value)
{
if ($value instanceof \Iterator) {
return $value;

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
final class Each
@ -22,15 +20,17 @@ final class Each
* @param mixed $iterable Iterator or array to iterate over.
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*/
public static function of(
$iterable,
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
'rejected' => $onRejected
]))->promise();
}
@ -46,17 +46,19 @@ final class Each
* @param int|callable $concurrency
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*/
public static function ofLimit(
$iterable,
$concurrency,
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
'concurrency' => $concurrency,
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
'concurrency' => $concurrency
]))->promise();
}
@ -68,17 +70,19 @@ final class Each
* @param mixed $iterable
* @param int|callable $concurrency
* @param callable $onFulfilled
*
* @return PromiseInterface
*/
public static function ofLimitAll(
$iterable,
$concurrency,
callable $onFulfilled = null
): PromiseInterface {
) {
return self::ofLimit(
$iterable,
$concurrency,
$onFulfilled,
function ($reason, $idx, PromiseInterface $aggregate): void {
function ($reason, $idx, PromiseInterface $aggregate) {
$aggregate->reject($reason);
}
);

View File

@ -1,14 +1,10 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
* Represents a promise that iterates over many promises and invokes
* side-effect functions in the process.
*
* @final
*/
class EachPromise implements PromisorInterface
{
@ -73,7 +69,7 @@ class EachPromise implements PromisorInterface
}
/** @psalm-suppress InvalidNullableReturnType */
public function promise(): PromiseInterface
public function promise()
{
if ($this->aggregate) {
return $this->aggregate;
@ -86,18 +82,21 @@ class EachPromise implements PromisorInterface
$this->refillPending();
} catch (\Throwable $e) {
$this->aggregate->reject($e);
} catch (\Exception $e) {
$this->aggregate->reject($e);
}
/**
* @psalm-suppress NullableReturnStatement
* @phpstan-ignore-next-line
*/
return $this->aggregate;
}
private function createPromise(): void
private function createPromise()
{
$this->mutex = false;
$this->aggregate = new Promise(function (): void {
$this->aggregate = new Promise(function () {
if ($this->checkIfFinished()) {
return;
}
@ -114,7 +113,7 @@ class EachPromise implements PromisorInterface
});
// Clear the references when the promise is resolved.
$clearFn = function (): void {
$clearFn = function () {
$this->iterable = $this->concurrency = $this->pending = null;
$this->onFulfilled = $this->onRejected = null;
$this->nextPendingIndex = 0;
@ -123,13 +122,11 @@ class EachPromise implements PromisorInterface
$this->aggregate->then($clearFn, $clearFn);
}
private function refillPending(): void
private function refillPending()
{
if (!$this->concurrency) {
// Add all pending promises.
while ($this->addPending() && $this->advanceIterator()) {
}
while ($this->addPending() && $this->advanceIterator());
return;
}
@ -150,11 +147,10 @@ class EachPromise implements PromisorInterface
// next value to yield until promise callbacks are called.
while (--$concurrency
&& $this->advanceIterator()
&& $this->addPending()) {
}
&& $this->addPending());
}
private function addPending(): bool
private function addPending()
{
if (!$this->iterable || !$this->iterable->valid()) {
return false;
@ -168,7 +164,7 @@ class EachPromise implements PromisorInterface
$idx = $this->nextPendingIndex++;
$this->pending[$idx] = $promise->then(
function ($value) use ($idx, $key): void {
function ($value) use ($idx, $key) {
if ($this->onFulfilled) {
call_user_func(
$this->onFulfilled,
@ -179,7 +175,7 @@ class EachPromise implements PromisorInterface
}
$this->step($idx);
},
function ($reason) use ($idx, $key): void {
function ($reason) use ($idx, $key) {
if ($this->onRejected) {
call_user_func(
$this->onRejected,
@ -195,7 +191,7 @@ class EachPromise implements PromisorInterface
return true;
}
private function advanceIterator(): bool
private function advanceIterator()
{
// Place a lock on the iterator so that we ensure to not recurse,
// preventing fatal generator errors.
@ -208,17 +204,19 @@ class EachPromise implements PromisorInterface
try {
$this->iterable->next();
$this->mutex = false;
return true;
} catch (\Throwable $e) {
$this->aggregate->reject($e);
$this->mutex = false;
return false;
} catch (\Exception $e) {
$this->aggregate->reject($e);
$this->mutex = false;
return false;
}
}
private function step(int $idx): void
private function step($idx)
{
// If the promise was already resolved, then ignore this step.
if (Is::settled($this->aggregate)) {
@ -236,12 +234,11 @@ class EachPromise implements PromisorInterface
}
}
private function checkIfFinished(): bool
private function checkIfFinished()
{
if (!$this->pending && !$this->iterable->valid()) {
// Resolve the promise if there's nothing left to do.
$this->aggregate->resolve(null);
return true;
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -9,16 +7,11 @@ namespace GuzzleHttp\Promise;
*
* Thenning off of this promise will invoke the onFulfilled callback
* immediately and ignore other callbacks.
*
* @final
*/
class FulfilledPromise implements PromiseInterface
{
private $value;
/**
* @param mixed $value
*/
public function __construct($value)
{
if (is_object($value) && method_exists($value, 'then')) {
@ -33,7 +26,7 @@ class FulfilledPromise implements PromiseInterface
public function then(
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
// Return itself if there is no onFulfilled function.
if (!$onFulfilled) {
return $this;
@ -42,12 +35,14 @@ class FulfilledPromise implements PromiseInterface
$queue = Utils::queue();
$p = new Promise([$queue, 'run']);
$value = $this->value;
$queue->add(static function () use ($p, $value, $onFulfilled): void {
$queue->add(static function () use ($p, $value, $onFulfilled) {
if (Is::pending($p)) {
try {
$p->resolve($onFulfilled($value));
} catch (\Throwable $e) {
$p->reject($e);
} catch (\Exception $e) {
$p->reject($e);
}
}
});
@ -55,34 +50,34 @@ class FulfilledPromise implements PromiseInterface
return $p;
}
public function otherwise(callable $onRejected): PromiseInterface
public function otherwise(callable $onRejected)
{
return $this->then(null, $onRejected);
}
public function wait(bool $unwrap = true)
public function wait($unwrap = true, $defaultDelivery = null)
{
return $unwrap ? $this->value : null;
}
public function getState(): string
public function getState()
{
return self::FULFILLED;
}
public function resolve($value): void
public function resolve($value)
{
if ($value !== $this->value) {
throw new \LogicException('Cannot resolve a fulfilled promise');
throw new \LogicException("Cannot resolve a fulfilled promise");
}
}
public function reject($reason): void
public function reject($reason)
{
throw new \LogicException('Cannot reject a fulfilled promise');
throw new \LogicException("Cannot reject a fulfilled promise");
}
public function cancel(): void
public function cancel()
{
// pass
}

View File

@ -1,39 +1,45 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
final class Is
{
/**
* Returns true if a promise is pending.
*
* @return bool
*/
public static function pending(PromiseInterface $promise): bool
public static function pending(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::PENDING;
}
/**
* Returns true if a promise is fulfilled or rejected.
*
* @return bool
*/
public static function settled(PromiseInterface $promise): bool
public static function settled(PromiseInterface $promise)
{
return $promise->getState() !== PromiseInterface::PENDING;
}
/**
* Returns true if a promise is fulfilled.
*
* @return bool
*/
public static function fulfilled(PromiseInterface $promise): bool
public static function fulfilled(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::FULFILLED;
}
/**
* Returns true if a promise is rejected.
*
* @return bool
*/
public static function rejected(PromiseInterface $promise): bool
public static function rejected(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::REJECTED;
}

View File

@ -1,15 +1,11 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
* Promises/A+ implementation that avoids recursion when possible.
*
* @see https://promisesaplus.com/
*
* @final
* @link https://promisesaplus.com/
*/
class Promise implements PromiseInterface
{
@ -35,36 +31,33 @@ class Promise implements PromiseInterface
public function then(
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
if ($this->state === self::PENDING) {
$p = new Promise(null, [$this, 'cancel']);
$this->handlers[] = [$p, $onFulfilled, $onRejected];
$p->waitList = $this->waitList;
$p->waitList[] = $this;
return $p;
}
// Return a fulfilled promise and immediately invoke any callbacks.
if ($this->state === self::FULFILLED) {
$promise = Create::promiseFor($this->result);
return $onFulfilled ? $promise->then($onFulfilled) : $promise;
}
// It's either cancelled or rejected, so return a rejected promise
// and immediately invoke any callbacks.
$rejection = Create::rejectionFor($this->result);
return $onRejected ? $rejection->then(null, $onRejected) : $rejection;
}
public function otherwise(callable $onRejected): PromiseInterface
public function otherwise(callable $onRejected)
{
return $this->then(null, $onRejected);
}
public function wait(bool $unwrap = true)
public function wait($unwrap = true)
{
$this->waitIfPending();
@ -80,12 +73,12 @@ class Promise implements PromiseInterface
}
}
public function getState(): string
public function getState()
{
return $this->state;
}
public function cancel(): void
public function cancel()
{
if ($this->state !== self::PENDING) {
return;
@ -100,6 +93,8 @@ class Promise implements PromiseInterface
$fn();
} catch (\Throwable $e) {
$this->reject($e);
} catch (\Exception $e) {
$this->reject($e);
}
}
@ -110,17 +105,17 @@ class Promise implements PromiseInterface
}
}
public function resolve($value): void
public function resolve($value)
{
$this->settle(self::FULFILLED, $value);
}
public function reject($reason): void
public function reject($reason)
{
$this->settle(self::REJECTED, $reason);
}
private function settle(string $state, $value): void
private function settle($state, $value)
{
if ($this->state !== self::PENDING) {
// Ignore calls with the same resolution.
@ -153,7 +148,7 @@ class Promise implements PromiseInterface
if (!is_object($value) || !method_exists($value, 'then')) {
$id = $state === self::FULFILLED ? 1 : 2;
// It's a success, so resolve the handlers in the queue.
Utils::queue()->add(static function () use ($id, $value, $handlers): void {
Utils::queue()->add(static function () use ($id, $value, $handlers) {
foreach ($handlers as $handler) {
self::callHandler($id, $value, $handler);
}
@ -164,12 +159,12 @@ class Promise implements PromiseInterface
} else {
// Resolve the handlers when the forwarded promise is resolved.
$value->then(
static function ($value) use ($handlers): void {
static function ($value) use ($handlers) {
foreach ($handlers as $handler) {
self::callHandler(1, $value, $handler);
}
},
static function ($reason) use ($handlers): void {
static function ($reason) use ($handlers) {
foreach ($handlers as $handler) {
self::callHandler(2, $reason, $handler);
}
@ -185,7 +180,7 @@ class Promise implements PromiseInterface
* @param mixed $value Value to pass to the callback.
* @param array $handler Array of handler data (promise and callbacks).
*/
private static function callHandler(int $index, $value, array $handler): void
private static function callHandler($index, $value, array $handler)
{
/** @var PromiseInterface $promise */
$promise = $handler[0];
@ -216,10 +211,12 @@ class Promise implements PromiseInterface
}
} catch (\Throwable $reason) {
$promise->reject($reason);
} catch (\Exception $reason) {
$promise->reject($reason);
}
}
private function waitIfPending(): void
private function waitIfPending()
{
if ($this->state !== self::PENDING) {
return;
@ -230,9 +227,9 @@ class Promise implements PromiseInterface
} else {
// If there's no wait function, then reject the promise.
$this->reject('Cannot wait on a promise that has '
.'no internal wait function. You must provide a wait '
.'function when constructing the promise to be able to '
.'wait on a promise.');
. 'no internal wait function. You must provide a wait '
. 'function when constructing the promise to be able to '
. 'wait on a promise.');
}
Utils::queue()->run();
@ -243,13 +240,13 @@ class Promise implements PromiseInterface
}
}
private function invokeWaitFn(): void
private function invokeWaitFn()
{
try {
$wfn = $this->waitFn;
$this->waitFn = null;
$wfn(true);
} catch (\Throwable $reason) {
} catch (\Exception $reason) {
if ($this->state === self::PENDING) {
// The promise has not been resolved yet, so reject the promise
// with the exception.
@ -262,7 +259,7 @@ class Promise implements PromiseInterface
}
}
private function invokeWaitList(): void
private function invokeWaitList()
{
$waitList = $this->waitList;
$this->waitList = null;

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -11,13 +9,13 @@ namespace GuzzleHttp\Promise;
* which registers callbacks to receive either a promises eventual value or
* the reason why the promise cannot be fulfilled.
*
* @see https://promisesaplus.com/
* @link https://promisesaplus.com/
*/
interface PromiseInterface
{
public const PENDING = 'pending';
public const FULFILLED = 'fulfilled';
public const REJECTED = 'rejected';
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/**
* Appends fulfillment and rejection handlers to the promise, and returns
@ -25,11 +23,13 @@ interface PromiseInterface
*
* @param callable $onFulfilled Invoked when the promise fulfills.
* @param callable $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface
*/
public function then(
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface;
);
/**
* Appends a rejection handler callback to the promise, and returns a new
@ -38,16 +38,20 @@ interface PromiseInterface
* fulfilled.
*
* @param callable $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface
*/
public function otherwise(callable $onRejected): PromiseInterface;
public function otherwise(callable $onRejected);
/**
* Get the state of the promise ("pending", "rejected", or "fulfilled").
*
* The three states can be checked against the constants defined on
* PromiseInterface: PENDING, FULFILLED, and REJECTED.
*
* @return string
*/
public function getState(): string;
public function getState();
/**
* Resolve the promise with the given value.
@ -56,7 +60,7 @@ interface PromiseInterface
*
* @throws \RuntimeException if the promise is already resolved.
*/
public function resolve($value): void;
public function resolve($value);
/**
* Reject the promise with the given reason.
@ -65,14 +69,14 @@ interface PromiseInterface
*
* @throws \RuntimeException if the promise is already resolved.
*/
public function reject($reason): void;
public function reject($reason);
/**
* Cancels the promise if possible.
*
* @see https://github.com/promises-aplus/cancellation-spec/issues/7
* @link https://github.com/promises-aplus/cancellation-spec/issues/7
*/
public function cancel(): void;
public function cancel();
/**
* Waits until the promise completes if possible.
@ -82,10 +86,12 @@ interface PromiseInterface
*
* If the promise cannot be waited on, then the promise will be rejected.
*
* @param bool $unwrap
*
* @return mixed
*
* @throws \LogicException if the promise has no wait function or if the
* promise does not settle after waiting.
*/
public function wait(bool $unwrap = true);
public function wait($unwrap = true);
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -11,6 +9,8 @@ interface PromisorInterface
{
/**
* Returns a promise.
*
* @return PromiseInterface
*/
public function promise(): PromiseInterface;
public function promise();
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -9,16 +7,11 @@ namespace GuzzleHttp\Promise;
*
* Thenning off of this promise will invoke the onRejected callback
* immediately and ignore other callbacks.
*
* @final
*/
class RejectedPromise implements PromiseInterface
{
private $reason;
/**
* @param mixed $reason
*/
public function __construct($reason)
{
if (is_object($reason) && method_exists($reason, 'then')) {
@ -33,7 +26,7 @@ class RejectedPromise implements PromiseInterface
public function then(
callable $onFulfilled = null,
callable $onRejected = null
): PromiseInterface {
) {
// If there's no onRejected callback then just return self.
if (!$onRejected) {
return $this;
@ -42,7 +35,7 @@ class RejectedPromise implements PromiseInterface
$queue = Utils::queue();
$reason = $this->reason;
$p = new Promise([$queue, 'run']);
$queue->add(static function () use ($p, $reason, $onRejected): void {
$queue->add(static function () use ($p, $reason, $onRejected) {
if (Is::pending($p)) {
try {
// Return a resolved promise if onRejected does not throw.
@ -50,6 +43,9 @@ class RejectedPromise implements PromiseInterface
} catch (\Throwable $e) {
// onRejected threw, so return a rejected promise.
$p->reject($e);
} catch (\Exception $e) {
// onRejected threw, so return a rejected promise.
$p->reject($e);
}
}
});
@ -57,12 +53,12 @@ class RejectedPromise implements PromiseInterface
return $p;
}
public function otherwise(callable $onRejected): PromiseInterface
public function otherwise(callable $onRejected)
{
return $this->then(null, $onRejected);
}
public function wait(bool $unwrap = true)
public function wait($unwrap = true, $defaultDelivery = null)
{
if ($unwrap) {
throw Create::exceptionFor($this->reason);
@ -71,24 +67,24 @@ class RejectedPromise implements PromiseInterface
return null;
}
public function getState(): string
public function getState()
{
return self::REJECTED;
}
public function resolve($value): void
public function resolve($value)
{
throw new \LogicException('Cannot resolve a rejected promise');
throw new \LogicException("Cannot resolve a rejected promise");
}
public function reject($reason): void
public function reject($reason)
{
if ($reason !== $this->reason) {
throw new \LogicException('Cannot reject a rejected promise');
throw new \LogicException("Cannot reject a rejected promise");
}
}
public function cancel(): void
public function cancel()
{
// pass
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -15,8 +13,8 @@ class RejectionException extends \RuntimeException
private $reason;
/**
* @param mixed $reason Rejection reason.
* @param string|null $description Optional description.
* @param mixed $reason Rejection reason.
* @param string $description Optional description
*/
public function __construct($reason, $description = null)
{
@ -25,13 +23,14 @@ class RejectionException extends \RuntimeException
$message = 'The promise was rejected';
if ($description) {
$message .= ' with reason: '.$description;
$message .= ' with reason: ' . $description;
} elseif (is_string($reason)
|| (is_object($reason) && method_exists($reason, '__toString'))
) {
$message .= ' with reason: '.$this->reason;
$message .= ' with reason: ' . $this->reason;
} elseif ($reason instanceof \JsonSerializable) {
$message .= ' with reason: '.json_encode($this->reason, JSON_PRETTY_PRINT);
$message .= ' with reason: '
. json_encode($this->reason, JSON_PRETTY_PRINT);
}
parent::__construct($message);

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
@ -12,18 +10,16 @@ namespace GuzzleHttp\Promise;
* by calling the `run()` function of the global task queue in an event loop.
*
* GuzzleHttp\Promise\Utils::queue()->run();
*
* @final
*/
class TaskQueue implements TaskQueueInterface
{
private $enableShutdown = true;
private $queue = [];
public function __construct(bool $withShutdown = true)
public function __construct($withShutdown = true)
{
if ($withShutdown) {
register_shutdown_function(function (): void {
register_shutdown_function(function () {
if ($this->enableShutdown) {
// Only run the tasks if an E_ERROR didn't occur.
$err = error_get_last();
@ -35,17 +31,17 @@ class TaskQueue implements TaskQueueInterface
}
}
public function isEmpty(): bool
public function isEmpty()
{
return !$this->queue;
}
public function add(callable $task): void
public function add(callable $task)
{
$this->queue[] = $task;
}
public function run(): void
public function run()
{
while ($task = array_shift($this->queue)) {
/** @var callable $task */
@ -64,7 +60,7 @@ class TaskQueue implements TaskQueueInterface
*
* Note: This shutdown will occur before any destructors are triggered.
*/
public function disableShutdown(): void
public function disableShutdown()
{
$this->enableShutdown = false;
}

View File

@ -1,24 +1,24 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
interface TaskQueueInterface
{
/**
* Returns true if the queue is empty.
*
* @return bool
*/
public function isEmpty(): bool;
public function isEmpty();
/**
* Adds a task to the queue that will be executed the next time run is
* called.
*/
public function add(callable $task): void;
public function add(callable $task);
/**
* Execute all of the pending task in the queue.
*/
public function run(): void;
public function run();
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
final class Utils
@ -19,9 +17,11 @@ final class Utils
* }
* </code>
*
* @param TaskQueueInterface|null $assign Optionally specify a new queue instance.
* @param TaskQueueInterface $assign Optionally specify a new queue instance.
*
* @return TaskQueueInterface
*/
public static function queue(TaskQueueInterface $assign = null): TaskQueueInterface
public static function queue(TaskQueueInterface $assign = null)
{
static $queue;
@ -39,18 +39,22 @@ final class Utils
* returns a promise that is fulfilled or rejected with the result.
*
* @param callable $task Task function to run.
*
* @return PromiseInterface
*/
public static function task(callable $task): PromiseInterface
public static function task(callable $task)
{
$queue = self::queue();
$promise = new Promise([$queue, 'run']);
$queue->add(function () use ($task, $promise): void {
$queue->add(function () use ($task, $promise) {
try {
if (Is::pending($promise)) {
$promise->resolve($task());
}
} catch (\Throwable $e) {
$promise->reject($e);
} catch (\Exception $e) {
$promise->reject($e);
}
});
@ -68,18 +72,22 @@ final class Utils
* key mapping to the rejection reason of the promise.
*
* @param PromiseInterface $promise Promise or value.
*
* @return array
*/
public static function inspect(PromiseInterface $promise): array
public static function inspect(PromiseInterface $promise)
{
try {
return [
'state' => PromiseInterface::FULFILLED,
'value' => $promise->wait(),
'value' => $promise->wait()
];
} catch (RejectionException $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
} catch (\Throwable $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
} catch (\Exception $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
}
@ -92,8 +100,10 @@ final class Utils
* @see inspect for the inspection state array format.
*
* @param PromiseInterface[] $promises Traversable of promises to wait upon.
*
* @return array
*/
public static function inspectAll($promises): array
public static function inspectAll($promises)
{
$results = [];
foreach ($promises as $key => $promise) {
@ -112,9 +122,12 @@ final class Utils
*
* @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on.
*
* @throws \Throwable on error
* @return array
*
* @throws \Exception on error
* @throws \Throwable on error in PHP >=7
*/
public static function unwrap($promises): array
public static function unwrap($promises)
{
$results = [];
foreach ($promises as $key => $promise) {
@ -134,21 +147,22 @@ final class Utils
*
* @param mixed $promises Promises or values.
* @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution.
*
* @return PromiseInterface
*/
public static function all($promises, bool $recursive = false): PromiseInterface
public static function all($promises, $recursive = false)
{
$results = [];
$promise = Each::of(
$promises,
function ($value, $idx) use (&$results): void {
function ($value, $idx) use (&$results) {
$results[$idx] = $value;
},
function ($reason, $idx, Promise $aggregate): void {
function ($reason, $idx, Promise $aggregate) {
$aggregate->reject($reason);
}
)->then(function () use (&$results) {
ksort($results);
return $results;
});
@ -159,7 +173,6 @@ final class Utils
return self::all($promises, $recursive);
}
}
return $results;
});
}
@ -180,15 +193,17 @@ final class Utils
*
* @param int $count Total number of promises.
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function some(int $count, $promises): PromiseInterface
public static function some($count, $promises)
{
$results = [];
$rejections = [];
return Each::of(
$promises,
function ($value, $idx, PromiseInterface $p) use (&$results, $count): void {
function ($value, $idx, PromiseInterface $p) use (&$results, $count) {
if (Is::settled($p)) {
return;
}
@ -197,7 +212,7 @@ final class Utils
$p->resolve(null);
}
},
function ($reason) use (&$rejections): void {
function ($reason) use (&$rejections) {
$rejections[] = $reason;
}
)->then(
@ -209,7 +224,6 @@ final class Utils
);
}
ksort($results);
return array_values($results);
}
);
@ -220,8 +234,10 @@ final class Utils
* fulfillment value is not an array of 1 but the value directly.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function any($promises): PromiseInterface
public static function any($promises)
{
return self::some(1, $promises)->then(function ($values) {
return $values[0];
@ -237,22 +253,23 @@ final class Utils
* @see inspect for the inspection state array format.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function settle($promises): PromiseInterface
public static function settle($promises)
{
$results = [];
return Each::of(
$promises,
function ($value, $idx) use (&$results): void {
function ($value, $idx) use (&$results) {
$results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
},
function ($reason, $idx) use (&$results): void {
function ($reason, $idx) use (&$results) {
$results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
}
)->then(function () use (&$results) {
ksort($results);
return $results;
});
}

View File

@ -0,0 +1,363 @@
<?php
namespace GuzzleHttp\Promise;
/**
* Get the global task queue used for promise resolution.
*
* This task queue MUST be run in an event loop in order for promises to be
* settled asynchronously. It will be automatically run when synchronously
* waiting on a promise.
*
* <code>
* while ($eventLoop->isRunning()) {
* GuzzleHttp\Promise\queue()->run();
* }
* </code>
*
* @param TaskQueueInterface $assign Optionally specify a new queue instance.
*
* @return TaskQueueInterface
*
* @deprecated queue will be removed in guzzlehttp/promises:2.0. Use Utils::queue instead.
*/
function queue(TaskQueueInterface $assign = null)
{
return Utils::queue($assign);
}
/**
* Adds a function to run in the task queue when it is next `run()` and returns
* a promise that is fulfilled or rejected with the result.
*
* @param callable $task Task function to run.
*
* @return PromiseInterface
*
* @deprecated task will be removed in guzzlehttp/promises:2.0. Use Utils::task instead.
*/
function task(callable $task)
{
return Utils::task($task);
}
/**
* Creates a promise for a value if the value is not a promise.
*
* @param mixed $value Promise or value.
*
* @return PromiseInterface
*
* @deprecated promise_for will be removed in guzzlehttp/promises:2.0. Use Create::promiseFor instead.
*/
function promise_for($value)
{
return Create::promiseFor($value);
}
/**
* Creates a rejected promise for a reason if the reason is not a promise. If
* the provided reason is a promise, then it is returned as-is.
*
* @param mixed $reason Promise or reason.
*
* @return PromiseInterface
*
* @deprecated rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead.
*/
function rejection_for($reason)
{
return Create::rejectionFor($reason);
}
/**
* Create an exception for a rejected promise value.
*
* @param mixed $reason
*
* @return \Exception|\Throwable
*
* @deprecated exception_for will be removed in guzzlehttp/promises:2.0. Use Create::exceptionFor instead.
*/
function exception_for($reason)
{
return Create::exceptionFor($reason);
}
/**
* Returns an iterator for the given value.
*
* @param mixed $value
*
* @return \Iterator
*
* @deprecated iter_for will be removed in guzzlehttp/promises:2.0. Use Create::iterFor instead.
*/
function iter_for($value)
{
return Create::iterFor($value);
}
/**
* Synchronously waits on a promise to resolve and returns an inspection state
* array.
*
* Returns a state associative array containing a "state" key mapping to a
* valid promise state. If the state of the promise is "fulfilled", the array
* will contain a "value" key mapping to the fulfilled value of the promise. If
* the promise is rejected, the array will contain a "reason" key mapping to
* the rejection reason of the promise.
*
* @param PromiseInterface $promise Promise or value.
*
* @return array
*
* @deprecated inspect will be removed in guzzlehttp/promises:2.0. Use Utils::inspect instead.
*/
function inspect(PromiseInterface $promise)
{
return Utils::inspect($promise);
}
/**
* Waits on all of the provided promises, but does not unwrap rejected promises
* as thrown exception.
*
* Returns an array of inspection state arrays.
*
* @see inspect for the inspection state array format.
*
* @param PromiseInterface[] $promises Traversable of promises to wait upon.
*
* @return array
*
* @deprecated inspect will be removed in guzzlehttp/promises:2.0. Use Utils::inspectAll instead.
*/
function inspect_all($promises)
{
return Utils::inspectAll($promises);
}
/**
* Waits on all of the provided promises and returns the fulfilled values.
*
* Returns an array that contains the value of each promise (in the same order
* the promises were provided). An exception is thrown if any of the promises
* are rejected.
*
* @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on.
*
* @return array
*
* @throws \Exception on error
* @throws \Throwable on error in PHP >=7
*
* @deprecated unwrap will be removed in guzzlehttp/promises:2.0. Use Utils::unwrap instead.
*/
function unwrap($promises)
{
return Utils::unwrap($promises);
}
/**
* Given an array of promises, return a promise that is fulfilled when all the
* items in the array are fulfilled.
*
* The promise's fulfillment value is an array with fulfillment values at
* respective positions to the original array. If any promise in the array
* rejects, the returned promise is rejected with the rejection reason.
*
* @param mixed $promises Promises or values.
* @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution.
*
* @return PromiseInterface
*
* @deprecated all will be removed in guzzlehttp/promises:2.0. Use Utils::all instead.
*/
function all($promises, $recursive = false)
{
return Utils::all($promises, $recursive);
}
/**
* Initiate a competitive race between multiple promises or values (values will
* become immediately fulfilled promises).
*
* When count amount of promises have been fulfilled, the returned promise is
* fulfilled with an array that contains the fulfillment values of the winners
* in order of resolution.
*
* This promise is rejected with a {@see AggregateException} if the number of
* fulfilled promises is less than the desired $count.
*
* @param int $count Total number of promises.
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*
* @deprecated some will be removed in guzzlehttp/promises:2.0. Use Utils::some instead.
*/
function some($count, $promises)
{
return Utils::some($count, $promises);
}
/**
* Like some(), with 1 as count. However, if the promise fulfills, the
* fulfillment value is not an array of 1 but the value directly.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*
* @deprecated any will be removed in guzzlehttp/promises:2.0. Use Utils::any instead.
*/
function any($promises)
{
return Utils::any($promises);
}
/**
* Returns a promise that is fulfilled when all of the provided promises have
* been fulfilled or rejected.
*
* The returned promise is fulfilled with an array of inspection state arrays.
*
* @see inspect for the inspection state array format.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*
* @deprecated settle will be removed in guzzlehttp/promises:2.0. Use Utils::settle instead.
*/
function settle($promises)
{
return Utils::settle($promises);
}
/**
* Given an iterator that yields promises or values, returns a promise that is
* fulfilled with a null value when the iterator has been consumed or the
* aggregate promise has been fulfilled or rejected.
*
* $onFulfilled is a function that accepts the fulfilled value, iterator index,
* and the aggregate promise. The callback can invoke any necessary side
* effects and choose to resolve or reject the aggregate if needed.
*
* $onRejected is a function that accepts the rejection reason, iterator index,
* and the aggregate promise. The callback can invoke any necessary side
* effects and choose to resolve or reject the aggregate if needed.
*
* @param mixed $iterable Iterator or array to iterate over.
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*
* @deprecated each will be removed in guzzlehttp/promises:2.0. Use Each::of instead.
*/
function each(
$iterable,
callable $onFulfilled = null,
callable $onRejected = null
) {
return Each::of($iterable, $onFulfilled, $onRejected);
}
/**
* Like each, but only allows a certain number of outstanding promises at any
* given time.
*
* $concurrency may be an integer or a function that accepts the number of
* pending promises and returns a numeric concurrency limit value to allow for
* dynamic a concurrency size.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*
* @deprecated each_limit will be removed in guzzlehttp/promises:2.0. Use Each::ofLimit instead.
*/
function each_limit(
$iterable,
$concurrency,
callable $onFulfilled = null,
callable $onRejected = null
) {
return Each::ofLimit($iterable, $concurrency, $onFulfilled, $onRejected);
}
/**
* Like each_limit, but ensures that no promise in the given $iterable argument
* is rejected. If any promise is rejected, then the aggregate promise is
* rejected with the encountered rejection.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @param callable $onFulfilled
*
* @return PromiseInterface
*
* @deprecated each_limit_all will be removed in guzzlehttp/promises:2.0. Use Each::ofLimitAll instead.
*/
function each_limit_all(
$iterable,
$concurrency,
callable $onFulfilled = null
) {
return Each::ofLimitAll($iterable, $concurrency, $onFulfilled);
}
/**
* Returns true if a promise is fulfilled.
*
* @return bool
*
* @deprecated is_fulfilled will be removed in guzzlehttp/promises:2.0. Use Is::fulfilled instead.
*/
function is_fulfilled(PromiseInterface $promise)
{
return Is::fulfilled($promise);
}
/**
* Returns true if a promise is rejected.
*
* @return bool
*
* @deprecated is_rejected will be removed in guzzlehttp/promises:2.0. Use Is::rejected instead.
*/
function is_rejected(PromiseInterface $promise)
{
return Is::rejected($promise);
}
/**
* Returns true if a promise is fulfilled or rejected.
*
* @return bool
*
* @deprecated is_settled will be removed in guzzlehttp/promises:2.0. Use Is::settled instead.
*/
function is_settled(PromiseInterface $promise)
{
return Is::settled($promise);
}
/**
* Create a new coroutine.
*
* @see Coroutine
*
* @return PromiseInterface
*
* @deprecated coroutine will be removed in guzzlehttp/promises:2.0. Use Coroutine::of instead.
*/
function coroutine(callable $generatorFn)
{
return Coroutine::of($generatorFn);
}

View File

@ -0,0 +1,6 @@
<?php
// Don't redefine the functions if included multiple times.
if (!function_exists('GuzzleHttp\Promise\promise_for')) {
require __DIR__ . '/functions.php';
}

View File

@ -1,9 +0,0 @@
{
"require": {
"php": "^7.4 || ^8.0",
"friendsofphp/php-cs-fixer": "3.16.0"
},
"config": {
"preferred-install": "dist"
}
}

View File

@ -1,10 +0,0 @@
{
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "1.10.11",
"phpstan/phpstan-deprecation-rules": "1.1.3"
},
"config": {
"preferred-install": "dist"
}
}

View File

@ -1,9 +0,0 @@
{
"require": {
"php": "^7.4 || ^8.0",
"psalm/phar": "5.9.0"
},
"config": {
"preferred-install": "dist"
}
}

View File

@ -26,7 +26,7 @@ $config = new Config();
return $config->setRules([
'@PER' => true,
'@PER:risky' => true,
'@PHP82Migration' => true,
'@PHP81Migration' => true,
'@PHPUnit84Migration:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'class_attributes_separation' => true,
@ -68,4 +68,4 @@ return $config->setRules([
],
])
->setFinder($finder)
->setRiskyAllowed(true);
->setRiskyAllowed(true);

View File

@ -1 +1 @@
php 8.2.5
php 8.2.0

View File

@ -14,17 +14,13 @@ version.
## Overview
A fast and simple streaming zip file downloader for PHP. Using this library will
save you from having to write the Zip to disk. You can directly send it to the
user, which is much faster. It can work with S3 buckets or any PSR7 Stream.
A fast and simple streaming zip file downloader for PHP. Using this library will save you from having to write the Zip to disk. You can directly send it to the user, which is much faster. It can work with S3 buckets or any PSR7 Stream.
Please see the [LICENSE](LICENSE) file for licensing and warranty information.
## Installation
Simply add a dependency on maennchen/zipstream-php to your project's
`composer.json` file if you use Composer to manage the dependencies of your
project. Use following command to add the package to your project's dependencies:
Simply add a dependency on maennchen/zipstream-php to your project's composer.json file if you use Composer to manage the dependencies of your project. Use following command to add the package to your project's dependencies:
```bash
composer require maennchen/zipstream-php
@ -33,116 +29,51 @@ composer require maennchen/zipstream-php
## Usage
For detailed instructions, please check the
[Documentation](https://maennchen.github.io/ZipStream-PHP/).
[Documentation](https://maennchen.dev/ZipStream-PHP/).
Here's a simple example:
```php
// Autoload the dependencies
require 'vendor/autoload.php';
// create a new zipstream object
$zip = new ZipStream\ZipStream(
outputName: 'example.zip',
// enable output of HTTP headers
$options = new ZipStream\Option\Archive();
$options->setSendHttpHeaders(true);
// enable output of HTTP headers
sendHttpHeaders: true,
);
// create a new zipstream object
$zip = new ZipStream\ZipStream('example.zip', $options);
// create a file named 'hello.txt'
$zip->addFile(
fileName: 'hello.txt',
data: 'This is the contents of hello.txt',
);
$zip->addFile('hello.txt', 'This is the contents of hello.txt');
// add a file named 'some_image.jpg' from a local file 'path/to/image.jpg'
$zip->addFileFromPath(
fileName: 'some_image.jpg',
path: 'path/to/image.jpg',
);
$zip->addFileFromPath('some_image.jpg', 'path/to/image.jpg');
// finish the zip stream
$zip->finish();
```
## Upgrade to version 3.0.0
### General
- Minimum PHP Version: `8.1`
- Only 64bit Architecture is supported.
- The class `ZipStream\Option\Method` has been replaced with the enum
`ZipStream\CompressionMethod`.
- Most clases have been flagged as `@internal` and should not be used from the
outside.
If you're using internal resources to extend this library, please open an
issue so that a clean interface can be added & published.
The externally available classes & enums are:
- `ZipStream\CompressionMethod`
- `ZipStream\Exception*`
- `ZipStream\ZipStream`
### Archive Options
- The class `ZipStream\Option\Archive` has been replaced in favor of named
arguments in the `ZipStream\ZipStream` constuctor.
- The archive options `largeFileSize` & `largeFileMethod` has been removed. If
you want different `compressionMethods` based on the file size, you'll have to
implement this yourself.
- The archive option `httpHeaderCallback` changed the type from `callable` to
`Closure`.
- The archive option `zeroHeader` has been replaced with the option
`defaultEnableZeroHeader` and can be overridden for every file. Its default
value changed from `false` to `true`.
- The archive option `statFiles` was removed since the library no longer checks
filesizes this way.
- The archive option `deflateLevel` has been replaced with the option
`defaultDeflateLevel` and can be overridden for every file.
- The first argument (`name`) of the `ZipStream\ZipStream` constuctor has been
replaced with the named argument `outputName`.
- Headers are now also sent if the `outputName` is empty. If you do not want to
automatically send http headers, set `sendHttpHeaders` to `false`.
### File Options
- The class `ZipStream\Option\File` has been replaced in favor of named
arguments in the `ZipStream\ZipStream->addFile*` functions.
- The file option `method` has been renamed to `compressionMethod`.
- The file option `time` has been renamed to `lastModificationDateTime`.
- The file option `size` has been renamed to `maxSize`.
## Upgrade to version 2.0.0
https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-200
- Only the self opened streams will be closed (#139)
If you were relying on ZipStream to close streams that the library didn't open,
you'll need to close them yourself now.
## Upgrade to version 1.0.0
https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-100
- All options parameters to all function have been moved from an `array` to structured option objects. See [the wiki](https://github.com/maennchen/ZipStream-PHP/wiki/Available-options) for examples.
- The whole library has been refactored. The minimal PHP requirement has been raised to PHP 7.1.
## Usage with Symfony and S3
You can find example code on [the wiki](https://github.com/maennchen/ZipStream-PHP/wiki/Symfony-example).
## Contributing
ZipStream-PHP is a collaborative project. Please take a look at the
[.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) file.
## Version Support
Versions are supported according to the table below.
Please do not open any pull requests contradicting the current version support
status.
Careful: Always check the `README` on `main` for up-to-date information.
| Version | New Features | Bugfixes | Security |
|---------|--------------|----------|----------|
| *3* | ✓ | ✓ | ✓ |
| *2* | ✗ | ✓ | ✓ |
| *1* | ✗ | ✗ | ✓ |
| *0* | ✗ | ✗ | ✗ |
This library aligns itself with the PHP core support. New features and bugfixes
will only target PHP versions according to their current status.
See: https://www.php.net/supported-versions.php
## About the Authors
- Paul Duncan <pabs@pablotron.org> - https://pablotron.org/

View File

@ -22,35 +22,26 @@
}
],
"require": {
"php-64bit": "^8.1",
"php": "^8.0",
"ext-mbstring": "*",
"ext-zlib": "*"
"psr/http-message": "^1.0",
"myclabs/php-enum": "^1.5"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"guzzlehttp/guzzle": "^7.5",
"phpunit/phpunit": "^8.5.8 || ^9.4.2",
"guzzlehttp/guzzle": "^6.5.3 || ^7.2.0",
"ext-zip": "*",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"friendsofphp/php-cs-fixer": "^3.16",
"vimeo/psalm": "^5.0"
},
"suggest": {
"psr/http-message": "^2.0",
"guzzlehttp/psr7": "^2.4"
"vimeo/psalm": "^5.0",
"php-coveralls/php-coveralls": "^2.4",
"friendsofphp/php-cs-fixer": "^3.9"
},
"scripts": {
"format": "php-cs-fixer fix",
"test": [
"@test:unit",
"@test:formatted",
"@test:lint"
],
"format": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix",
"test": "composer run test:unit && composer run test:formatted && composer run test:lint",
"test:unit": "phpunit --coverage-clover=coverage.clover.xml --coverage-html cov",
"test:unit:slow": "@test:unit --group slow",
"test:unit:fast": "@test:unit --exclude-group slow",
"test:formatted": "@format --dry-run --stop-on-violation --using-cache=no",
"test:lint": "psalm --stats --show-info=true --find-unused-psalm-suppress",
"test:formatted": "composer run format -- --dry-run --stop-on-violation --using-cache=no",
"test:lint": "psalm --stats --show-info --find-unused-psalm-suppress",
"coverage:report": "php-coveralls --coverage_clover=coverage.clover.xml --json_path=coveralls-upload.json --insecure",
"install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656",
"docs:generate": "tools/phpdocumentor --sourcecode"
@ -60,9 +51,6 @@
"ZipStream\\": "src/"
}
},
"autoload-dev": {
"psr-4": { "ZipStream\\Test\\": "test/" }
},
"archive": {
"exclude": [
"/composer.lock",

View File

@ -1,47 +1,79 @@
Adding Content-Length header
=============
Adding a ``Content-Length`` header for ``ZipStream`` can be achieved by
using the options ``SIMULATION_STRICT`` or ``SIMULATION_LAX`` in the
``operationMode`` parameter.
Adding a ``Content-Length`` header for ``ZipStream`` is not trivial since the
size is not known beforehand.
In the ``SIMULATION_STRICT`` mode, ``ZipStream`` will not allow to calculate the
size based on reading the whole file. ``SIMULATION_LAX`` will read the whole
file if neccessary.
``SIMULATION_STRICT`` is therefore useful to make sure that the size can be
calculated efficiently.
The following workaround adds an approximated header:
.. code-block:: php
use ZipStream\OperationMode;
use ZipStream\ZipStream;
$zip = new ZipStream(
operationMode: OperationMode::SIMULATE_STRICT, // or SIMULATE_LAX
defaultEnableZeroHeader: false,
sendHttpHeaders: true,
outputStream: $stream,
);
class Zip
{
/** @var string */
private $name;
// Normally add files
$zip->addFile('sample.txt', 'Sample String Data');
private $files = [];
// Use addFileFromCallback and exactSize if you want to defer opening of
// the file resource
$zip->addFileFromCallback(
'sample.txt',
exactSize: 18,
callback: function () {
return fopen('...');
public function __construct($name)
{
$this->name = $name;
}
);
// Read resulting file size
$size = $zip->finish();
// Tell it to the browser
header('Content-Length: '. $size);
// Execute the Simulation and stream the actual zip to the client
$zip->executeSimulation();
public function addFile($name, $data)
{
$this->files[] = ['type' => 'addFile', 'name' => $name, 'data' => $data];
}
public function addFileFromPath($name, $path)
{
$this->files[] = ['type' => 'addFileFromPath', 'name' => $name, 'path' => $path];
}
public function getEstimate()
{
$estimate = 22;
foreach ($this->files as $file) {
$estimate += 76 + 2 * strlen($file['name']);
if ($file['type'] === 'addFile') {
$estimate += strlen($file['data']);
}
if ($file['type'] === 'addFileFromPath') {
$estimate += filesize($file['path']);
}
}
return $estimate;
}
public function finish()
{
header('Content-Length: ' . $this->getEstimate());
$options = new \ZipStream\Option\Archive();
$options->setSendHttpHeaders(true);
$options->setEnableZip64(false);
$options->setDeflateLevel(-1);
$zip = new \ZipStream\ZipStream($this->name, $options);
$fileOptions = new \ZipStream\Option\File();
$fileOptions->setMethod(\ZipStream\Option\Method::STORE());
foreach ($this->files as $file) {
if ($file['type'] === 'addFile') {
$zip->addFile($file['name'], $file['data'], $fileOptions);
}
if ($file['type'] === 'addFileFromPath') {
$zip->addFileFromPath($file['name'], $file['path'], $fileOptions);
}
}
$zip->finish();
exit;
}
}
It only works with the following constraints:
- All file content is known beforehand.
- Content Deflation is disabled
Thanks to
`partiellkorrekt <https://github.com/maennchen/ZipStream-PHP/issues/89#issuecomment-1047949274>`_
for this workaround.

View File

@ -14,21 +14,20 @@ default one, and pass it to Flysystem ``putStream`` method.
// the content is lost when closing the stream / opening another one
$tempStream = fopen('php://memory', 'w+');
// Init Options
$zipStreamOptions = new Archive();
$zipStreamOptions->setOutputStream($tempStream);
// Create Zip Archive
$zipStream = new ZipStream(
outputStream: $tempStream,
outputName: 'test.zip',
);
$zipStream = new ZipStream('test.zip', $zipStreamOptions);
$zipStream->addFile('test.txt', 'text');
$zipStream->finish();
// Store File
// (see Flysystem documentation, and all its framework integration)
// Can be any adapter (AWS, Google, Ftp, etc.)
$adapter = new Local(__DIR__.'/path/to/folder');
// Store File (see Flysystem documentation, and all its framework integration)
$adapter = new Local(__DIR__.'/path/to/folder'); // Can be any adapter (AWS, Google, Ftp, etc.)
$filesystem = new Filesystem($adapter);
$filesystem->writeStream('test.zip', $tempStream)
$filesystem->putStream('test.zip', $tempStream)
// Close Stream
fclose($tempStream);
fclose($tempStream);

View File

@ -2,65 +2,60 @@ Available options
===============
Here is the full list of options available to you. You can also have a look at
``src/ZipStream.php`` file.
``src/Option/Archive.php`` file.
First, an instance of ``ZipStream\Option\Archive`` needs to be created, and
after that you use setters methods to modify the values.
.. code-block:: php
use ZipStream\ZipStream;
use ZipStream\Option\Archive as ArchiveOptions;
require_once 'vendor/autoload.php';
$zip = new ZipStream(
// Define output stream
// (argument is eiter a resource or implementing
// `Psr\Http\Message\StreamInterface`)
//
// Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies
// required when using `Psr\Http\Message\StreamInterface`.
outputStream: $filePointer,
$opt = new ArchiveOptions();
// Set the deflate level (default is 6; use -1 to disable it)
defaultDeflateLevel: 6,
// Define output stream (argument is of type resource)
$opt->setOutputStream($fd);
// Add a comment to the zip file
comment: 'This is a comment.',
// Set the deflate level (default is 6; use -1 to disable it)
$opt->setDeflateLevel(6);
// Send http headers (default is true)
sendHttpHeaders: false,
// Add a comment to the zip file
$opt->setComment('This is a comment.');
// HTTP Content-Disposition.
// Defaults to 'attachment', where FILENAME is the specified filename.
// Note that this does nothing if you are not sending HTTP headers.
contentDisposition: 'attachment',
// Size, in bytes, of the largest file to try and load into memory (used by addFileFromPath()). Large files may also be compressed differently; see the 'largeFileMethod' option.
$opt->setLargeFileSize(30000000);
// Output Name for HTTP Content-Disposition
// Defaults to no name
outputName: "example.zip",
// How to handle large files. Legal values are STORE (the default), or DEFLATE. Store sends the file raw and is significantly faster, while DEFLATE compresses the file and is much, much slower. Note that deflate must compress the file twice and is extremely slow.
$opt->setLargeFileMethod(ZipStream\Option\Method::STORE());
$opt->setLargeFileMethod(ZipStream\Option\Method::DEFLATE());
// HTTP Content-Type.
// Defaults to 'application/x-zip'.
// Note that this does nothing if you are not sending HTTP headers.
contentType: 'application/x-zip',
// Send http headers (default is false)
$opt->setSendHttpHeaders(false);
// Set the function called for setting headers.
// Default is the `header()` of PHP
httpHeaderCallback: header(...),
// HTTP Content-Disposition. Defaults to 'attachment', where FILENAME is the specified filename. Note that this does nothing if you are not sending HTTP headers.
$opt->setContentDisposition('attachment');
// Enable streaming files with single read where general purpose bit 3
// indicates local file header contain zero values in crc and size
// fields, these appear only after file contents in data descriptor
// block.
// Set to true if your input stream is remote
// (used with addFileFromStream()).
// Default is false.
defaultEnableZeroHeader: false,
// Set the content type (does nothing if you are not sending HTTP headers)
$opt->setContentType('application/x-zip');
// Enable zip64 extension, allowing very large archives
// (> 4Gb or file count > 64k)
// Default is true
enableZip64: true,
// Set the function called for setting headers. Default is the `header()` of PHP
$opt->setHttpHeaderCallback('header');
// Flush output buffer after every write
// Default is false
flushOutput: true,
);
// Enable streaming files with single read where general purpose bit 3 indicates local file header contain zero values in crc and size fields, these appear only after file contents in data descriptor block. Default is false. Set to true if your input stream is remote (used with addFileFromStream()).
$opt->setZeroHeader(false);
// Enable reading file stat for determining file size. When a 32-bit system reads file size that is over 2 GB, invalid value appears in file size due to integer overflow. Should be disabled on 32-bit systems with method addFileFromPath if any file may exceed 2 GB. In this case file will be read in blocks and correct size will be determined from content. Default is true.
$opt->setStatFiles(true);
// Enable zip64 extension, allowing very large archives (> 4Gb or file count > 64k)
// default is true
$opt->setEnableZip64(true);
// Flush output buffer after every write
// default is false
$opt->setFlushOutput(true);
// Now that everything is set you can pass the options to the ZipStream instance
$zip = new ZipStream('example.zip', $opt);

View File

@ -12,10 +12,7 @@ Example
---------------
.. code-block:: php
$stream = $response->getBody();
// add a file named 'streamfile.txt' from the content of the stream
$zip->addFileFromPsr7Stream(
fileName: 'streamfile.txt',
stream: $stream,
);
$zip->addFileFromPsr7Stream('streamfile.txt', $stream);

View File

@ -5,9 +5,9 @@ Stream to S3 Bucket
---------------
.. code-block:: php
use Aws\S3\S3Client;
use Aws\Credentials\CredentialProvider;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
$bucket = 'your bucket name';
@ -21,19 +21,13 @@ Stream to S3 Bucket
$zipFile = fopen("s3://$bucket/example.zip", 'w');
$zip = new ZipStream(
enableZip64: false,
outputStream: $zipFile,
);
$options = new Archive();
$options->setEnableZip64(false);
$options->setOutputStream($zipFile);
$zip->addFile(
fileName: 'file1.txt',
data: 'File1 data',
);
$zip->addFile(
fileName: 'file2.txt',
data: 'File2 data',
);
$zip = new ZipStream(null, $options);
$zip->addFile('file1.txt', 'File1 data');
$zip->addFile('file2.txt', 'File2 data');
$zip->finish();
fclose($zipFile);
fclose($zipFile);

View File

@ -31,7 +31,7 @@ stored in an AWS S3 bucket by key:
*/
public function zipStreamAction()
{
// sample test file on s3
//sample test file on s3
$s3keys = array(
"ziptestfolder/file1.txt"
);
@ -39,18 +39,18 @@ stored in an AWS S3 bucket by key:
$s3Client = $this->get('app.amazon.s3'); //s3client service
$s3Client->registerStreamWrapper(); //required
// using StreamedResponse to wrap ZipStream functionality
// for files on AWS s3.
//using StreamedResponse to wrap ZipStream functionality for files on AWS s3.
$response = new StreamedResponse(function() use($s3keys, $s3Client)
{
// Define suitable options for ZipStream Archive.
$options = new \ZipStream\Option\Archive();
$options->setContentType('application/octet-stream');
// this is needed to prevent issues with truncated zip files
$options->setZeroHeader(true);
$options->setComment('test zip file.');
//initialise zipstream with output zip filename and options.
$zip = new ZipStream\ZipStream(
outputName: 'test.zip',
defaultEnableZeroHeader: true,
contentType: 'application/octet-stream',
);
$zip = new ZipStream\ZipStream('test.zip', $options);
//loop keys - useful for multiple files
foreach ($s3keys as $key) {
@ -58,19 +58,15 @@ stored in an AWS S3 bucket by key:
//file using the same name.
$fileName = basename($key);
// concatenate s3path.
// replace with your bucket name or get from parameters file.
$bucket = 'bucketname';
//concatenate s3path.
$bucket = 'bucketname'; //replace with your bucket name or get from parameters file.
$s3path = "s3://" . $bucket . "/" . $key;
//addFileFromStream
if ($streamRead = fopen($s3path, 'r')) {
$zip->addFileFromStream(
fileName: $fileName,
stream: $streamRead,
);
$zip->addFileFromStream($fileName, $streamRead);
} else {
die('Could not open stream for reading');
die('Could not open stream for reading');
}
}
@ -127,4 +123,4 @@ You need to add correct permissions
's3' => ['ACL' => 'public-read'],
]);
fopen($path, 'w', null, $outputContext);
fopen($path, 'w', null, $outputContext);

View File

@ -22,20 +22,11 @@ Installation
Simply add a dependency on ``maennchen/zipstream-php`` to your project's
``composer.json`` file if you use Composer to manage the dependencies of your
project. Use following command to add the package to your project's
dependencies:
project. Use following command to add the package to your project's dependencies:
.. code-block:: sh
composer require maennchen/zipstream-php
If you want to use``addFileFromPsr7Stream```
(``Psr\Http\Message\StreamInterface``) or use a stream instead of a
``resource`` as ``outputStream``, the following dependencies must be installed
as well:
.. code-block:: sh
composer require psr/http-message guzzlehttp/psr7
If ``composer install`` yields the following error, your installation is missing
the `mbstring extension <https://www.php.net/manual/en/book.mbstring.php>`_,
either `install it <https://www.php.net/manual/en/mbstring.installation.php>`_
@ -61,42 +52,25 @@ Here's a simple example:
// Autoload the dependencies
require 'vendor/autoload.php';
// create a new zipstream object
$zip = new ZipStream\ZipStream(
outputName: 'example.zip',
// enable output of HTTP headers
$options = new ZipStream\Option\Archive();
$options->setSendHttpHeaders(true);
// enable output of HTTP headers
sendHttpHeaders: true,
);
// create a new zipstream object
$zip = new ZipStream\ZipStream('example.zip', $options);
// create a file named 'hello.txt'
$zip->addFile(
fileName: 'hello.txt',
data: 'This is the contents of hello.txt',
);
$zip->addFile('hello.txt', 'This is the contents of hello.txt');
// add a file named 'some_image.jpg' from a local file 'path/to/image.jpg'
$zip->addFileFromPath(
fileName: 'some_image.jpg',
path: 'path/to/image.jpg',
);
$zip->addFileFromPath('some_image.jpg', 'path/to/image.jpg');
// add a file named 'goodbye.txt' from an open stream resource
$filePointer = tmpfile();
fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.');
rewind($filePointer);
$zip->addFileFromStream(
fileName: 'goodbye.txt',
stream: $filePointer,
);
fclose($filePointer);
// add a file named 'streamfile.txt' from the body of a `guzzle` response
// Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies required.
$zip->addFileFromPsr7Stream(
fileName: 'streamfile.txt',
stream: $response->getBody(),
);
$fp = tmpfile();
fwrite($fp, 'The quick brown fox jumped over the lazy dog.');
rewind($fp);
$zip->addFileFromStream('goodbye.txt', $fp);
fclose($fp);
// finish the zip stream
$zip->finish();

View File

@ -1,15 +1,14 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="test/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" cacheDirectory=".phpunit.cache">
<coverage/>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="test/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Application">
<directory>test</directory>
</testsuite>
</testsuites>
<logging/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View File

@ -1,12 +1,9 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
>
<projectFiles>
<directory name="src" />
@ -14,10 +11,43 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<!-- Turn off dead code warnings for externally called functions -->
<PossiblyUnusedProperty errorLevel="suppress" />
<PossiblyUnusedMethod errorLevel="suppress" />
<PossiblyUnusedReturnValue errorLevel="suppress" />
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<DeprecatedProperty errorLevel="info" />
<DeprecatedClass errorLevel="info" />
<DeprecatedConstant errorLevel="info" />
<DeprecatedFunction errorLevel="info" />
<DeprecatedInterface errorLevel="info" />
<DeprecatedTrait errorLevel="info" />
<InternalMethod errorLevel="info" />
<InternalProperty errorLevel="info" />
<InternalClass errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<MissingClosureParamType errorLevel="info" />
<MissingParamType errorLevel="info" />
<RedundantCondition errorLevel="info" />
<DocblockTypeContradiction errorLevel="info" />
<RedundantConditionGivenDocblockType errorLevel="info" />
<UnresolvableInclude errorLevel="info" />
<RawObjectIteration errorLevel="info" />
<InvalidStringClass errorLevel="info" />
</issueHandlers>
</psalm>

View File

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use OverflowException;
class Bigint
{
/**
* @var int[]
*/
private $bytes = [0, 0, 0, 0, 0, 0, 0, 0];
/**
* Initialize the bytes array
*
* @param int $value
*/
public function __construct(int $value = 0)
{
$this->fillBytes($value, 0, 8);
}
/**
* Get an instance
*
* @param int $value
* @return Bigint
*/
public static function init(int $value = 0): self
{
return new self($value);
}
/**
* Fill bytes from low to high
*
* @param int $low
* @param int $high
* @return Bigint
*/
public static function fromLowHigh(int $low, int $high): self
{
$bigint = new self();
$bigint->fillBytes($low, 0, 4);
$bigint->fillBytes($high, 4, 4);
return $bigint;
}
/**
* Get high 32
*
* @return int
*/
public function getHigh32(): int
{
return $this->getValue(4, 4);
}
/**
* Get value from bytes array
*
* @param int $end
* @param int $length
* @return int
*/
public function getValue(int $end = 0, int $length = 8): int
{
$result = 0;
for ($i = $end + $length - 1; $i >= $end; $i--) {
$result <<= 8;
$result |= $this->bytes[$i];
}
return $result;
}
/**
* Get low FF
*
* @param bool $force
* @return float
*/
public function getLowFF(bool $force = false): float
{
if ($force || $this->isOver32()) {
return (float)0xFFFFFFFF;
}
return (float)$this->getLow32();
}
/**
* Check if is over 32
*
* @psalm-suppress ArgumentTypeCoercion
* @param bool $force
* @return bool
*/
public function isOver32(bool $force = false): bool
{
// value 0xFFFFFFFF already needs a Zip64 header
return $force ||
max(array_slice($this->bytes, 4, 4)) > 0 ||
min(array_slice($this->bytes, 0, 4)) === 0xFF;
}
/**
* Get low 32
*
* @return int
*/
public function getLow32(): int
{
return $this->getValue(0, 4);
}
/**
* Get hexadecimal
*
* @return string
*/
public function getHex64(): string
{
$result = '0x';
for ($i = 7; $i >= 0; $i--) {
$result .= sprintf('%02X', $this->bytes[$i]);
}
return $result;
}
/**
* Add
*
* @param Bigint $other
* @return Bigint
*/
public function add(self $other): self
{
$result = clone $this;
$overflow = false;
for ($i = 0; $i < 8; $i++) {
$result->bytes[$i] += $other->bytes[$i];
if ($overflow) {
$result->bytes[$i]++;
$overflow = false;
}
if ($result->bytes[$i] & 0x100) {
$overflow = true;
$result->bytes[$i] &= 0xFF;
}
}
if ($overflow) {
throw new OverflowException();
}
return $result;
}
/**
* Fill the bytes field with int
*
* @param int $value
* @param int $start
* @param int $count
* @return void
*/
protected function fillBytes(int $value, int $start, int $count): void
{
for ($i = 0; $i < $count; $i++) {
$this->bytes[$start + $i] = $i >= PHP_INT_SIZE ? 0 : $value & 0xFF;
$value >>= 8;
}
}
}

View File

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use DateTimeInterface;
/**
* @internal
*/
abstract class CentralDirectoryFileHeader
{
private const SIGNATURE = 0x02014b50;
public static function generate(
int $versionMadeBy,
int $versionNeededToExtract,
int $generalPurposeBitFlag,
CompressionMethod $compressionMethod,
DateTimeInterface $lastModificationDateTime,
int $crc32,
int $compressedSize,
int $uncompressedSize,
string $fileName,
string $extraField,
string $fileComment,
int $diskNumberStart,
int $internalFileAttributes,
int $externalFileAttributes,
int $relativeOffsetOfLocalHeader,
): string {
return PackField::pack(
new PackField(format: 'V', value: self::SIGNATURE),
new PackField(format: 'v', value: $versionMadeBy),
new PackField(format: 'v', value: $versionNeededToExtract),
new PackField(format: 'v', value: $generalPurposeBitFlag),
new PackField(format: 'v', value: $compressionMethod->value),
new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)),
new PackField(format: 'V', value: $crc32),
new PackField(format: 'V', value: $compressedSize),
new PackField(format: 'V', value: $uncompressedSize),
new PackField(format: 'v', value: strlen($fileName)),
new PackField(format: 'v', value: strlen($extraField)),
new PackField(format: 'v', value: strlen($fileComment)),
new PackField(format: 'v', value: $diskNumberStart),
new PackField(format: 'v', value: $internalFileAttributes),
new PackField(format: 'V', value: $externalFileAttributes),
new PackField(format: 'V', value: $relativeOffsetOfLocalHeader),
) . $fileName . $extraField . $fileComment;
}
}

View File

@ -1,106 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
enum CompressionMethod: int
{
/**
* The file is stored (no compression)
*/
case STORE = 0x00;
// 0x01: legacy algorithm - The file is Shrunk
// 0x02: legacy algorithm - The file is Reduced with compression factor 1
// 0x03: legacy algorithm - The file is Reduced with compression factor 2
// 0x04: legacy algorithm - The file is Reduced with compression factor 3
// 0x05: legacy algorithm - The file is Reduced with compression factor 4
// 0x06: legacy algorithm - The file is Imploded
// 0x07: Reserved for Tokenizing compression algorithm
/**
* The file is Deflated
*/
case DEFLATE = 0x08;
// /**
// * Enhanced Deflating using Deflate64(tm)
// */
// case DEFLATE_64 = 0x09;
// /**
// * PKWARE Data Compression Library Imploding (old IBM TERSE)
// */
// case PKWARE = 0x0a;
// // 0x0b: Reserved by PKWARE
// /**
// * File is compressed using BZIP2 algorithm
// */
// case BZIP2 = 0x0c;
// // 0x0d: Reserved by PKWARE
// /**
// * LZMA
// */
// case LZMA = 0x0e;
// // 0x0f: Reserved by PKWARE
// /**
// * IBM z/OS CMPSC Compression
// */
// case IBM_ZOS_CMPSC = 0x10;
// // 0x11: Reserved by PKWARE
// /**
// * File is compressed using IBM TERSE
// */
// case IBM_TERSE = 0x12;
// /**
// * IBM LZ77 z Architecture
// */
// case IBM_LZ77 = 0x13;
// // 0x14: deprecated (use method 93 for zstd)
// /**
// * Zstandard (zstd) Compression
// */
// case ZSTD = 0x5d;
// /**
// * MP3 Compression
// */
// case MP3 = 0x5e;
// /**
// * XZ Compression
// */
// case XZ = 0x5f;
// /**
// * JPEG variant
// */
// case JPEG = 0x60;
// /**
// * WavPack compressed data
// */
// case WAV_PACK = 0x61;
// /**
// * PPMd version I, Rev 1
// */
// case PPMD_1_1 = 0x62;
// /**
// * AE-x encryption marker
// */
// case AE_X_ENCRYPTION = 0x63;
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
/**
* @internal
*/
abstract class DataDescriptor
{
private const SIGNATURE = 0x08074b50;
public static function generate(
int $crc32UncompressedData,
int $compressedSize,
int $uncompressedSize,
): string {
return PackField::pack(
new PackField(format: 'V', value: self::SIGNATURE),
new PackField(format: 'V', value: $crc32UncompressedData),
new PackField(format: 'V', value: $compressedSize),
new PackField(format: 'V', value: $uncompressedSize),
);
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace ZipStream;
/**
* @deprecated
*/
class DeflateStream extends Stream
{
public function __construct($stream)
{
parent::__construct($stream);
trigger_error('Class ' . __CLASS__ . ' is deprecated, delation will be handled internally instead', E_USER_DEPRECATED);
}
public function removeDeflateFilter(): void
{
trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
}
public function addDeflateFilter(Option\File $options): void
{
trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
}
}

View File

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
/**
* @internal
*/
abstract class EndOfCentralDirectory
{
private const SIGNATURE = 0x06054b50;
public static function generate(
int $numberOfThisDisk,
int $numberOfTheDiskWithCentralDirectoryStart,
int $numberOfCentralDirectoryEntriesOnThisDisk,
int $numberOfCentralDirectoryEntries,
int $sizeOfCentralDirectory,
int $centralDirectoryStartOffsetOnDisk,
string $zipFileComment,
): string {
/** @psalm-suppress MixedArgument */
return PackField::pack(
new PackField(format: 'V', value: static::SIGNATURE),
new PackField(format: 'v', value: $numberOfThisDisk),
new PackField(format: 'v', value: $numberOfTheDiskWithCentralDirectoryStart),
new PackField(format: 'v', value: $numberOfCentralDirectoryEntriesOnThisDisk),
new PackField(format: 'v', value: $numberOfCentralDirectoryEntries),
new PackField(format: 'V', value: $sizeOfCentralDirectory),
new PackField(format: 'V', value: $centralDirectoryStartOffsetOnDisk),
new PackField(format: 'v', value: strlen($zipFileComment)),
) . $zipFileComment;
}
}

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace ZipStream;
/**
* This class is only for inheriting
*/
abstract class Exception extends \Exception
{
}

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use DateTimeInterface;
use ZipStream\Exception;
/**
* This Exception gets invoked if a file wasn't found
*/
class DosTimeOverflowException extends Exception
{
/**
* @internal
*/
public function __construct(
public readonly DateTimeInterface $dateTime
) {
parent::__construct('The date ' . $dateTime->format(DateTimeInterface::ATOM) . " can't be represented as DOS time / date.");
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if file or comment encoding is incorrect
*/
class EncodingException extends Exception
{
}

View File

@ -12,11 +12,12 @@ use ZipStream\Exception;
class FileNotFoundException extends Exception
{
/**
* @internal
* Constructor of the Exception
*
* @param String $path - The path which wasn't found
*/
public function __construct(
public readonly string $path
) {
public function __construct(string $path)
{
parent::__construct("The file with the path $path wasn't found.");
}
}

View File

@ -12,11 +12,12 @@ use ZipStream\Exception;
class FileNotReadableException extends Exception
{
/**
* @internal
* Constructor of the Exception
*
* @param String $path - The path which wasn't found
*/
public function __construct(
public readonly string $path
) {
public function __construct(string $path)
{
parent::__construct("The file with the path $path isn't readable.");
}
}

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a file is not as large as it was specified.
*/
class FileSizeIncorrectException extends Exception
{
/**
* @internal
*/
public function __construct(
public readonly int $expectedSize,
public readonly int $actualSize
) {
parent::__construct("File is {$actualSize} instead of {$expectedSize} bytes large. Adjust `exactSize` parameter.");
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if options are incompatible
*/
class IncompatibleOptionsException extends Exception
{
}

View File

@ -11,9 +11,6 @@ use ZipStream\Exception;
*/
class OverflowException extends Exception
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.');

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a resource like `fread` returns false
*/
class ResourceActionException extends Exception
{
/**
* @var ?resource
*/
public $resource;
/**
* @param resource $resource
*/
public function __construct(
public readonly string $function,
$resource = null,
) {
$this->resource = $resource;
parent::__construct('Function ' . $function . 'failed on resource.');
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a strict simulation is executed and the file
* information can't be determined without reading the entire file.
*/
class SimulationFileUnknownException extends Exception
{
public function __construct()
{
parent::__construct('The details of the strict simulation file could not be determined without reading the entire file.');
}
}

View File

@ -7,15 +7,17 @@ namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a stream can't be read.
* This Exception gets invoked if `fread` fails on a stream.
*/
class StreamNotReadableException extends Exception
{
/**
* @internal
* Constructor of the Exception
*
* @param string $fileName - The name of the file which the stream belongs to.
*/
public function __construct()
public function __construct(string $fileName)
{
parent::__construct('The stream could not be read.');
parent::__construct("The stream for $fileName could not be read.");
}
}

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a non seekable stream is
* provided and zero headers are disabled.
*/
class StreamNotSeekableException extends Exception
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('enableZeroHeader must be enable to add non seekable streams');
}
}

View File

@ -4,417 +4,467 @@ declare(strict_types=1);
namespace ZipStream;
use Closure;
use DateTimeInterface;
use DeflateContext;
use RuntimeException;
use ZipStream\Exception\FileSizeIncorrectException;
use HashContext;
use Psr\Http\Message\StreamInterface;
use ZipStream\Exception\FileNotFoundException;
use ZipStream\Exception\FileNotReadableException;
use ZipStream\Exception\OverflowException;
use ZipStream\Exception\ResourceActionException;
use ZipStream\Exception\SimulationFileUnknownException;
use ZipStream\Exception\StreamNotReadableException;
use ZipStream\Exception\StreamNotSeekableException;
use ZipStream\Option\File as FileOptions;
use ZipStream\Option\Method;
use ZipStream\Option\Version;
/**
* @internal
*/
class File
{
private const CHUNKED_READ_BLOCK_SIZE = 0x1000000;
public const HASH_ALGORITHM = 'crc32b';
private Version $version;
public const BIT_ZERO_HEADER = 0x0008;
private int $compressedSize = 0;
public const BIT_EFS_UTF8 = 0x0800;
private int $uncompressedSize = 0;
public const COMPUTE = 1;
private int $crc = 0;
public const SEND = 2;
private int $generalPurposeBitFlag = 0;
private readonly string $fileName;
private const CHUNKED_READ_BLOCK_SIZE = 1048576;
/**
* @var resource|null
* @var string
*/
private $stream;
public $name;
/**
* @param Closure $dataCallback
* @psalm-param Closure(): resource $dataCallback
* @var FileOptions
*/
public function __construct(
string $fileName,
private readonly Closure $dataCallback,
private readonly OperationMode $operationMode,
private readonly int $startOffset,
private readonly CompressionMethod $compressionMethod,
private readonly string $comment,
private readonly DateTimeInterface $lastModificationDateTime,
private readonly int $deflateLevel,
private readonly ?int $maxSize,
private readonly ?int $exactSize,
private readonly bool $enableZip64,
private readonly bool $enableZeroHeader,
private readonly Closure $send,
private readonly Closure $recordSentBytes,
) {
$this->fileName = self::filterFilename($fileName);
$this->checkEncoding();
public $opt;
if ($this->enableZeroHeader) {
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER;
}
/**
* @var Bigint
*/
public $len;
$this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE;
/**
* @var Bigint
*/
public $zlen;
/** @var int */
public $crc;
/**
* @var Bigint
*/
public $hlen;
/**
* @var Bigint
*/
public $ofs;
/**
* @var int
*/
public $bits;
/**
* @var Version
*/
public $version;
/**
* @var ZipStream
*/
public $zip;
/**
* @var resource
*/
private $deflate;
/**
* @var HashContext
*/
private $hash;
/**
* @var Method
*/
private $method;
/**
* @var Bigint
*/
private $totalLength;
public function __construct(ZipStream $zip, string $name, ?FileOptions $opt = null)
{
$this->zip = $zip;
$this->name = $name;
$this->opt = $opt ?: new FileOptions();
$this->method = $this->opt->getMethod();
$this->version = Version::STORE();
$this->ofs = new Bigint();
}
public function cloneSimulationExecution(): self
public function processPath(string $path): void
{
return new self(
$this->fileName,
$this->dataCallback,
OperationMode::NORMAL,
$this->startOffset,
$this->compressionMethod,
$this->comment,
$this->lastModificationDateTime,
$this->deflateLevel,
$this->maxSize,
$this->exactSize,
$this->enableZip64,
$this->enableZeroHeader,
$this->send,
$this->recordSentBytes,
);
}
public function process(): string
{
$forecastSize = $this->forecastSize();
if ($this->enableZeroHeader) {
// No calculation required
} elseif ($this->isSimulation() && $forecastSize) {
$this->uncompressedSize = $forecastSize;
$this->compressedSize = $forecastSize;
} else {
$this->readStream(send: false);
if (rewind($this->unpackStream()) === false) {
throw new ResourceActionException('rewind', $this->unpackStream());
if (!is_readable($path)) {
if (!file_exists($path)) {
throw new FileNotFoundException($path);
}
throw new FileNotReadableException($path);
}
$this->addFileHeader();
$detectedSize = $forecastSize ?? $this->compressedSize;
if (
$this->isSimulation() &&
$detectedSize > 0
) {
($this->recordSentBytes)($detectedSize);
if ($this->zip->isLargeFile($path) === false) {
$data = file_get_contents($path);
$this->processData($data);
} else {
$this->readStream(send: true);
$this->method = $this->zip->opt->getLargeFileMethod();
$stream = new Stream(fopen($path, 'rb'));
$this->processStream($stream);
$stream->close();
}
}
public function processData(string $data): void
{
$this->len = new Bigint(strlen($data));
$this->crc = crc32($data);
// compress data if needed
if ($this->method->equals(Method::DEFLATE())) {
$data = gzdeflate($data);
}
$this->zlen = new Bigint(strlen($data));
$this->addFileHeader();
$this->zip->send($data);
$this->addFileFooter();
return $this->getCdrFile();
}
/**
* @return resource
*/
private function unpackStream()
{
if ($this->stream) {
return $this->stream;
}
if ($this->operationMode === OperationMode::SIMULATE_STRICT) {
throw new SimulationFileUnknownException();
}
$this->stream = ($this->dataCallback)();
if (!$this->enableZeroHeader && !stream_get_meta_data($this->stream)['seekable']) {
throw new StreamNotSeekableException();
}
if (!(
str_contains(stream_get_meta_data($this->stream)['mode'], 'r')
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'w+')
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'a+')
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'x+')
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'c+')
)) {
throw new StreamNotReadableException();
}
return $this->stream;
}
private function forecastSize(): ?int
{
if ($this->compressionMethod !== CompressionMethod::STORE) {
return null;
}
if ($this->exactSize) {
return $this->exactSize;
}
$fstat = fstat($this->unpackStream());
if (!$fstat || !array_key_exists('size', $fstat) || $fstat['size'] < 1) {
return null;
}
if ($this->maxSize !== null && $this->maxSize < $fstat['size']) {
return $this->maxSize;
}
return $fstat['size'];
}
/**
* Create and send zip header for this file.
*
* @return void
* @throws \ZipStream\Exception\EncodingException
*/
private function addFileHeader(): void
public function addFileHeader(): void
{
$forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64;
$name = static::filterFilename($this->name);
$footer = $this->buildZip64ExtraBlock($forceEnableZip64);
// calculate name length
$nameLength = strlen($name);
$zip64Enabled = $footer !== '';
// create dos timestamp
$time = static::dosTime($this->opt->getTime()->getTimestamp());
if($zip64Enabled) {
$this->version = Version::ZIP64;
$comment = $this->opt->getComment();
if (!mb_check_encoding($name, 'ASCII') ||
!mb_check_encoding($comment, 'ASCII')) {
// Sets Bit 11: Language encoding flag (EFS). If this bit is set,
// the filename and comment fields for this file
// MUST be encoded using UTF-8. (see APPENDIX D)
if (mb_check_encoding($name, 'UTF-8') &&
mb_check_encoding($comment, 'UTF-8')) {
$this->bits |= self::BIT_EFS_UTF8;
}
}
if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) {
// Put the tricky entry to
// force Linux unzip to lookup EFS flag.
$footer .= Zs\ExtendedInformationExtraField::generate();
if ($this->method->equals(Method::DEFLATE())) {
$this->version = Version::DEFLATE();
}
$data = LocalFileHeader::generate(
versionNeededToExtract: $this->version->value,
generalPurposeBitFlag: $this->generalPurposeBitFlag,
compressionMethod: $this->compressionMethod,
lastModificationDateTime: $this->lastModificationDateTime,
crc32UncompressedData: $this->crc,
compressedSize: $zip64Enabled
? 0xFFFFFFFF
: $this->compressedSize,
uncompressedSize: $zip64Enabled
? 0xFFFFFFFF
: $this->uncompressedSize,
fileName: $this->fileName,
extraField: $footer,
);
$force = (bool)($this->bits & self::BIT_ZERO_HEADER) &&
$this->zip->opt->isEnableZip64();
$footer = $this->buildZip64ExtraBlock($force);
($this->send)($data);
// If this file will start over 4GB limit in ZIP file,
// CDR record will have to use Zip64 extension to describe offset
// to keep consistency we use the same value here
if ($this->zip->ofs->isOver32()) {
$this->version = Version::ZIP64();
}
$fields = [
['V', ZipStream::FILE_HEADER_SIGNATURE],
['v', $this->version->getValue()], // Version needed to Extract
['v', $this->bits], // General purpose bit flags - data descriptor flag set
['v', $this->method->getValue()], // Compression method
['V', $time], // Timestamp (DOS Format)
['V', $this->crc], // CRC32 of data (0 -> moved to data descriptor footer)
['V', $this->zlen->getLowFF($force)], // Length of compressed data (forced to 0xFFFFFFFF for zero header)
['V', $this->len->getLowFF($force)], // Length of original data (forced to 0xFFFFFFFF for zero header)
['v', $nameLength], // Length of filename
['v', strlen($footer)], // Extra data (see above)
];
// pack fields and calculate "total" length
$header = ZipStream::packFields($fields);
// print header and filename
$data = $header . $name . $footer;
$this->zip->send($data);
// save header length
$this->hlen = Bigint::init(strlen($data));
}
/**
* Strip characters that are not legal in Windows filenames
* to prevent compatibility issues
*
* @param string $filename Unprocessed filename
* @return string
*/
private static function filterFilename(
/**
* Unprocessed filename
*/
string $fileName
): string {
public static function filterFilename(string $filename): string
{
// strip leading slashes from file name
// (fixes bug in windows archive viewer)
$fileName = ltrim($fileName, '/');
$filename = preg_replace('/^\\/+/', '', $filename);
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName);
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename);
}
private function checkEncoding(): void
/**
* Create and send data descriptor footer for this file.
*
* @return void
*/
public function addFileFooter(): void
{
// Sets Bit 11: Language encoding flag (EFS). If this bit is set,
// the filename and comment fields for this file
// MUST be encoded using UTF-8. (see APPENDIX D)
if (mb_check_encoding($this->fileName, 'UTF-8') &&
mb_check_encoding($this->comment, 'UTF-8')) {
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::EFS;
}
}
if ($this->bits & self::BIT_ZERO_HEADER) {
// compressed and uncompressed size
$sizeFormat = 'V';
if ($this->zip->opt->isEnableZip64()) {
$sizeFormat = 'P';
}
$fields = [
['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE],
['V', $this->crc], // CRC32
[$sizeFormat, $this->zlen], // Length of compressed data
[$sizeFormat, $this->len], // Length of original data
];
private function buildZip64ExtraBlock(bool $force = false): string
{
$outputZip64ExtraBlock = false;
$originalSize = null;
if ($force || $this->uncompressedSize > 0xFFFFFFFF) {
$outputZip64ExtraBlock = true;
$originalSize = $this->uncompressedSize;
}
$compressedSize = null;
if ($force || $this->compressedSize > 0xFFFFFFFF) {
$outputZip64ExtraBlock = true;
$compressedSize = $this->compressedSize;
}
// If this file will start over 4GB limit in ZIP file,
// CDR record will have to use Zip64 extension to describe offset
// to keep consistency we use the same value here
$relativeHeaderOffset = null;
if ($this->startOffset > 0xFFFFFFFF) {
$outputZip64ExtraBlock = true;
$relativeHeaderOffset = $this->startOffset;
}
if (!$outputZip64ExtraBlock) {
return '';
}
if (!$this->enableZip64) {
throw new OverflowException();
}
return Zip64\ExtendedInformationExtraField::generate(
originalSize: $originalSize,
compressedSize: $compressedSize,
relativeHeaderOffset: $relativeHeaderOffset,
diskStartNumber: null,
);
}
private function addFileFooter(): void
{
if (($this->compressedSize > 0xFFFFFFFF || $this->uncompressedSize > 0xFFFFFFFF) && $this->version !== Version::ZIP64) {
throw new OverflowException();
}
if (!$this->enableZeroHeader) {
return;
}
if ($this->version === Version::ZIP64) {
$footer = Zip64\DataDescriptor::generate(
crc32UncompressedData: $this->crc,
compressedSize: $this->compressedSize,
uncompressedSize: $this->uncompressedSize,
);
$footer = ZipStream::packFields($fields);
$this->zip->send($footer);
} else {
$footer = DataDescriptor::generate(
crc32UncompressedData: $this->crc,
compressedSize: $this->compressedSize,
uncompressedSize: $this->uncompressedSize,
);
$footer = '';
}
($this->send)($footer);
$this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer)));
$this->zip->addToCdr($this);
}
private function readStream(bool $send): void
public function processStream(StreamInterface $stream): void
{
$this->compressedSize = 0;
$this->uncompressedSize = 0;
$hash = hash_init('crc32b');
$this->zlen = new Bigint();
$this->len = new Bigint();
$deflate = $this->compressionInit();
while (
!feof($this->unpackStream()) &&
($this->maxSize === null || $this->uncompressedSize < $this->maxSize) &&
($this->exactSize === null || $this->uncompressedSize < $this->exactSize)
) {
$readLength = min(
($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize,
($this->exactSize ?? PHP_INT_MAX) - $this->uncompressedSize,
self::CHUNKED_READ_BLOCK_SIZE
);
$data = fread($this->unpackStream(), $readLength);
hash_update($hash, $data);
$this->uncompressedSize += strlen($data);
if ($deflate) {
$data = deflate_add(
$deflate,
$data,
feof($this->unpackStream()) ? ZLIB_FINISH : ZLIB_NO_FLUSH
);
}
$this->compressedSize += strlen($data);
if ($send) {
($this->send)($data);
}
}
if ($this->exactSize && $this->uncompressedSize !== $this->exactSize) {
throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize);
}
$this->crc = hexdec(hash_final($hash));
}
private function compressionInit(): ?DeflateContext
{
switch($this->compressionMethod) {
case CompressionMethod::STORE:
// Noting to do
return null;
case CompressionMethod::DEFLATE:
$deflateContext = deflate_init(
ZLIB_ENCODING_RAW,
['level' => $this->deflateLevel]
);
if (!$deflateContext) {
// @codeCoverageIgnoreStart
throw new RuntimeException("Can't initialize deflate context.");
// @codeCoverageIgnoreEnd
}
// False positive, resource is no longer returned from this function
return $deflateContext;
default:
// @codeCoverageIgnoreStart
throw new RuntimeException('Unsupported Compression Method ' . print_r($this->compressionMethod, true));
// @codeCoverageIgnoreEnd
if ($this->zip->opt->isZeroHeader()) {
$this->processStreamWithZeroHeader($stream);
} else {
$this->processStreamWithComputedHeader($stream);
}
}
private function getCdrFile(): string
/**
* Send CDR record for specified file.
*
* @return string
*/
public function getCdrFile(): string
{
$name = static::filterFilename($this->name);
// get attributes
$comment = $this->opt->getComment();
// get dos timestamp
$time = static::dosTime($this->opt->getTime()->getTimestamp());
$footer = $this->buildZip64ExtraBlock();
return CentralDirectoryFileHeader::generate(
versionMadeBy: ZipStream::ZIP_VERSION_MADE_BY,
versionNeededToExtract:$this->version->value,
generalPurposeBitFlag: $this->generalPurposeBitFlag,
compressionMethod: $this->compressionMethod,
lastModificationDateTime: $this->lastModificationDateTime,
crc32: $this->crc,
compressedSize: $this->compressedSize > 0xFFFFFFFF
? 0xFFFFFFFF
: $this->compressedSize,
uncompressedSize: $this->uncompressedSize > 0xFFFFFFFF
? 0xFFFFFFFF
: $this->uncompressedSize,
fileName: $this->fileName,
extraField: $footer,
fileComment: $this->comment,
diskNumberStart: 0,
internalFileAttributes: 0,
externalFileAttributes: 32,
relativeOffsetOfLocalHeader: $this->startOffset > 0xFFFFFFFF
? 0xFFFFFFFF
: $this->startOffset,
);
$fields = [
['V', ZipStream::CDR_FILE_SIGNATURE], // Central file header signature
['v', ZipStream::ZIP_VERSION_MADE_BY], // Made by version
['v', $this->version->getValue()], // Extract by version
['v', $this->bits], // General purpose bit flags - data descriptor flag set
['v', $this->method->getValue()], // Compression method
['V', $time], // Timestamp (DOS Format)
['V', $this->crc], // CRC32
['V', $this->zlen->getLowFF()], // Compressed Data Length
['V', $this->len->getLowFF()], // Original Data Length
['v', strlen($name)], // Length of filename
['v', strlen($footer)], // Extra data len (see above)
['v', strlen($comment)], // Length of comment
['v', 0], // Disk number
['v', 0], // Internal File Attributes
['V', 32], // External File Attributes
['V', $this->ofs->getLowFF()], // Relative offset of local header
];
// pack fields, then append name and comment
$header = ZipStream::packFields($fields);
return $header . $name . $footer . $comment;
}
private function isSimulation(): bool
/**
* @return Bigint
*/
public function getTotalLength(): Bigint
{
return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT;
return $this->totalLength;
}
/**
* Convert a UNIX timestamp to a DOS timestamp.
*
* @param int $when
* @return int DOS Timestamp
*/
final protected static function dosTime(int $when): int
{
// get date array for timestamp
$d = getdate($when);
// set lower-bound on dates
if ($d['year'] < 1980) {
$d = [
'year' => 1980,
'mon' => 1,
'mday' => 1,
'hours' => 0,
'minutes' => 0,
'seconds' => 0,
];
}
// remove extra years from 1980
$d['year'] -= 1980;
// return date string
return
($d['year'] << 25) |
($d['mon'] << 21) |
($d['mday'] << 16) |
($d['hours'] << 11) |
($d['minutes'] << 5) |
($d['seconds'] >> 1);
}
protected function buildZip64ExtraBlock(bool $force = false): string
{
$fields = [];
if ($this->len->isOver32($force)) {
$fields[] = ['P', $this->len]; // Length of original data
}
if ($this->len->isOver32($force)) {
$fields[] = ['P', $this->zlen]; // Length of compressed data
}
if ($this->ofs->isOver32()) {
$fields[] = ['P', $this->ofs]; // Offset of local header record
}
if (!empty($fields)) {
if (!$this->zip->opt->isEnableZip64()) {
throw new OverflowException();
}
array_unshift(
$fields,
['v', 0x0001], // 64 bit extension
['v', count($fields) * 8] // Length of data block
);
$this->version = Version::ZIP64();
}
if ($this->bits & self::BIT_EFS_UTF8) {
// Put the tricky entry to
// force Linux unzip to lookup EFS flag.
$fields[] = ['v', 0x5653]; // Choose 'ZS' for proprietary usage
$fields[] = ['v', 0x0000]; // zero length
}
return ZipStream::packFields($fields);
}
protected function processStreamWithZeroHeader(StreamInterface $stream): void
{
$this->bits |= self::BIT_ZERO_HEADER;
$this->addFileHeader();
$this->readStream($stream, self::COMPUTE | self::SEND);
$this->addFileFooter();
}
protected function readStream(StreamInterface $stream, ?int $options = null): void
{
$this->deflateInit();
$total = 0;
$size = $this->opt->getSize();
while (!$stream->eof() && ($size === 0 || $total < $size)) {
$data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE);
$total += strlen($data);
if ($size > 0 && $total > $size) {
$data = substr($data, 0, strlen($data)-($total - $size));
}
$this->deflateData($stream, $data, $options);
if ($options & self::SEND) {
$this->zip->send($data);
}
}
$this->deflateFinish($options);
}
protected function deflateInit(): void
{
$hash = hash_init(self::HASH_ALGORITHM);
$this->hash = $hash;
if ($this->method->equals(Method::DEFLATE())) {
$this->deflate = deflate_init(
ZLIB_ENCODING_RAW,
['level' => $this->opt->getDeflateLevel()]
);
}
}
protected function deflateData(StreamInterface $stream, string &$data, ?int $options = null): void
{
if ($options & self::COMPUTE) {
$this->len = $this->len->add(Bigint::init(strlen($data)));
hash_update($this->hash, $data);
}
if ($this->deflate) {
$data = deflate_add(
$this->deflate,
$data,
$stream->eof()
? ZLIB_FINISH
: ZLIB_NO_FLUSH
);
}
if ($options & self::COMPUTE) {
$this->zlen = $this->zlen->add(Bigint::init(strlen($data)));
}
}
protected function deflateFinish(?int $options = null): void
{
if ($options & self::COMPUTE) {
$this->crc = hexdec(hash_final($this->hash));
}
}
protected function processStreamWithComputedHeader(StreamInterface $stream): void
{
$this->readStream($stream, self::COMPUTE);
$stream->rewind();
$this->addFileHeader();
$this->readStream($stream, self::SEND);
$this->addFileFooter();
}
}

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
/**
* @internal
*/
abstract class GeneralPurposeBitFlag
{
/**
* If set, indicates that the file is encrypted.
*/
public const ENCRYPTED = 1 << 0;
/**
* (For Methods 8 and 9 - Deflating)
* Normal (-en) compression option was used.
*/
public const DEFLATE_COMPRESSION_NORMAL = 0 << 1;
/**
* (For Methods 8 and 9 - Deflating)
* Maximum (-exx/-ex) compression option was used.
*/
public const DEFLATE_COMPRESSION_MAXIMUM = 1 << 1;
/**
* (For Methods 8 and 9 - Deflating)
* Fast (-ef) compression option was used.
*/
public const DEFLATE_COMPRESSION_FAST = 10 << 1;
/**
* (For Methods 8 and 9 - Deflating)
* Super Fast (-es) compression option was used.
*/
public const DEFLATE_COMPRESSION_SUPERFAST = 11 << 1;
/**
* If the compression method used was type 14,
* LZMA, then this bit, if set, indicates
* an end-of-stream (EOS) marker is used to
* mark the end of the compressed data stream.
* If clear, then an EOS marker is not present
* and the compressed data size must be known
* to extract.
*/
public const LZMA_EOS = 1 << 1;
/**
* If this bit is set, the fields crc-32, compressed
* size and uncompressed size are set to zero in the
* local header. The correct values are put in the
* data descriptor immediately following the compressed
* data.
*/
public const ZERO_HEADER = 1 << 3;
/**
* If this bit is set, this indicates that the file is
* compressed patched data.
*/
public const COMPRESSED_PATCHED_DATA = 1 << 5;
/**
* Strong encryption. If this bit is set, you MUST
* set the version needed to extract value to at least
* 50 and you MUST also set bit 0. If AES encryption
* is used, the version needed to extract value MUST
* be at least 51.
*/
public const STRONG_ENCRYPTION = 1 << 6;
/**
* Language encoding flag (EFS). If this bit is set,
* the filename and comment fields for this file
* MUST be encoded using UTF-8.
*/
public const EFS = 1 << 11;
/**
* Set when encrypting the Central Directory to indicate
* selected data values in the Local Header are masked to
* hide their actual values.
*/
public const ENCRYPT_CENTRAL_DIRECTORY = 1 << 13;
}

View File

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use DateTimeInterface;
/**
* @internal
*/
abstract class LocalFileHeader
{
private const SIGNATURE = 0x04034b50;
public static function generate(
int $versionNeededToExtract,
int $generalPurposeBitFlag,
CompressionMethod $compressionMethod,
DateTimeInterface $lastModificationDateTime,
int $crc32UncompressedData,
int $compressedSize,
int $uncompressedSize,
string $fileName,
string $extraField,
): string {
return PackField::pack(
new PackField(format: 'V', value: self::SIGNATURE),
new PackField(format: 'v', value: $versionNeededToExtract),
new PackField(format: 'v', value: $generalPurposeBitFlag),
new PackField(format: 'v', value: $compressionMethod->value),
new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)),
new PackField(format: 'V', value: $crc32UncompressedData),
new PackField(format: 'V', value: $compressedSize),
new PackField(format: 'V', value: $uncompressedSize),
new PackField(format: 'v', value: strlen($fileName)),
new PackField(format: 'v', value: strlen($extraField)),
) . $fileName . $extraField;
}
}

View File

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
/**
* ZipStream execution operation modes
*/
enum OperationMode
{
/**
* Stream file into output stream
*/
case NORMAL;
/**
* Simulate the zip to figure out the resulting file size
*
* This only supports entries where the file size is known beforehand and
* deflation is disabled.
*/
case SIMULATE_STRICT;
/**
* Simulate the zip to figure out the resulting file size
*
* If the file size is not known beforehand or deflation is enabled, the
* entry streams will be read and rewound.
*
* If the entry does not support rewinding either, you will not be able to
* use the same stream in a later operation mode like `NORMAL`.
*/
case SIMULATE_LAX;
}

View File

@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace ZipStream\Option;
use Psr\Http\Message\StreamInterface;
final class Archive
{
public const DEFAULT_DEFLATE_LEVEL = 6;
/**
* @var string
*/
private $comment = '';
/**
* Size, in bytes, of the largest file to try
* and load into memory (used by
* addFileFromPath()). Large files may also
* be compressed differently; see the
* 'largeFileMethod' option. Default is ~20 Mb.
*
* @var int
*/
private $largeFileSize = 20 * 1024 * 1024;
/**
* How to handle large files. Legal values are
* Method::STORE() (the default), or
* Method::DEFLATE(). STORE sends the file
* raw and is significantly
* faster, while DEFLATE compresses the file
* and is much, much slower. Note that DEFLATE
* must compress the file twice and is extremely slow.
*
* @var Method
*/
private $largeFileMethod;
/**
* Boolean indicating whether or not to send
* the HTTP headers for this file.
*
* @var bool
*/
private $sendHttpHeaders = false;
/**
* The method called to send headers
*
* @var Callable
*/
private $httpHeaderCallback = 'header';
/**
* Enable Zip64 extension, supporting very large
* archives (any size > 4 GB or file count > 64k)
*
* @var bool
*/
private $enableZip64 = true;
/**
* Enable streaming files with single read where
* general purpose bit 3 indicates local file header
* contain zero values in crc and size fields,
* these appear only after file contents
* in data descriptor block.
*
* @var bool
*/
private $zeroHeader = false;
/**
* Enable reading file stat for determining file size.
* When a 32-bit system reads file size that is
* over 2 GB, invalid value appears in file size
* due to integer overflow. Should be disabled on
* 32-bit systems with method addFileFromPath
* if any file may exceed 2 GB. In this case file
* will be read in blocks and correct size will be
* determined from content.
*
* @var bool
*/
private $statFiles = true;
/**
* Enable flush after every write to output stream.
* @var bool
*/
private $flushOutput = false;
/**
* HTTP Content-Disposition. Defaults to
* 'attachment', where
* FILENAME is the specified filename.
*
* Note that this does nothing if you are
* not sending HTTP headers.
*
* @var string
*/
private $contentDisposition = 'attachment';
/**
* Note that this does nothing if you are
* not sending HTTP headers.
*
* @var string
*/
private $contentType = 'application/x-zip';
/**
* @var int
*/
private $deflateLevel = 6;
/**
* @var StreamInterface|resource
*/
private $outputStream;
/**
* Options constructor.
*/
public function __construct()
{
$this->largeFileMethod = Method::STORE();
$this->outputStream = fopen('php://output', 'wb');
}
public function getComment(): string
{
return $this->comment;
}
public function setComment(string $comment): void
{
$this->comment = $comment;
}
public function getLargeFileSize(): int
{
return $this->largeFileSize;
}
public function setLargeFileSize(int $largeFileSize): void
{
$this->largeFileSize = $largeFileSize;
}
public function getLargeFileMethod(): Method
{
return $this->largeFileMethod;
}
public function setLargeFileMethod(Method $largeFileMethod): void
{
$this->largeFileMethod = $largeFileMethod;
}
public function isSendHttpHeaders(): bool
{
return $this->sendHttpHeaders;
}
public function setSendHttpHeaders(bool $sendHttpHeaders): void
{
$this->sendHttpHeaders = $sendHttpHeaders;
}
public function getHttpHeaderCallback(): callable
{
return $this->httpHeaderCallback;
}
public function setHttpHeaderCallback(callable $httpHeaderCallback): void
{
$this->httpHeaderCallback = $httpHeaderCallback;
}
public function isEnableZip64(): bool
{
return $this->enableZip64;
}
public function setEnableZip64(bool $enableZip64): void
{
$this->enableZip64 = $enableZip64;
}
public function isZeroHeader(): bool
{
return $this->zeroHeader;
}
public function setZeroHeader(bool $zeroHeader): void
{
$this->zeroHeader = $zeroHeader;
}
public function isFlushOutput(): bool
{
return $this->flushOutput;
}
public function setFlushOutput(bool $flushOutput): void
{
$this->flushOutput = $flushOutput;
}
public function isStatFiles(): bool
{
return $this->statFiles;
}
public function setStatFiles(bool $statFiles): void
{
$this->statFiles = $statFiles;
}
public function getContentDisposition(): string
{
return $this->contentDisposition;
}
public function setContentDisposition(string $contentDisposition): void
{
$this->contentDisposition = $contentDisposition;
}
public function getContentType(): string
{
return $this->contentType;
}
public function setContentType(string $contentType): void
{
$this->contentType = $contentType;
}
/**
* @return StreamInterface|resource
*/
public function getOutputStream()
{
return $this->outputStream;
}
/**
* @param StreamInterface|resource $outputStream
*/
public function setOutputStream($outputStream): void
{
$this->outputStream = $outputStream;
}
/**
* @return int
*/
public function getDeflateLevel(): int
{
return $this->deflateLevel;
}
/**
* @param int $deflateLevel
*/
public function setDeflateLevel(int $deflateLevel): void
{
$this->deflateLevel = $deflateLevel;
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace ZipStream\Option;
use DateTime;
use DateTimeInterface;
final class File
{
/**
* @var string
*/
private $comment = '';
/**
* @var Method
*/
private $method;
/**
* @var int
*/
private $deflateLevel;
/**
* @var DateTimeInterface
*/
private $time;
/**
* @var int
*/
private $size = 0;
public function defaultTo(Archive $archiveOptions): void
{
$this->deflateLevel = $this->deflateLevel ?: $archiveOptions->getDeflateLevel();
$this->time = $this->time ?: new DateTime();
}
/**
* @return string
*/
public function getComment(): string
{
return $this->comment;
}
/**
* @param string $comment
*/
public function setComment(string $comment): void
{
$this->comment = $comment;
}
/**
* @return Method
*/
public function getMethod(): Method
{
return $this->method ?: Method::DEFLATE();
}
/**
* @param Method $method
*/
public function setMethod(Method $method): void
{
$this->method = $method;
}
/**
* @return int
*/
public function getDeflateLevel(): int
{
return $this->deflateLevel ?: Archive::DEFAULT_DEFLATE_LEVEL;
}
/**
* @param int $deflateLevel
*/
public function setDeflateLevel(int $deflateLevel): void
{
$this->deflateLevel = $deflateLevel;
}
/**
* @return DateTimeInterface
*/
public function getTime(): DateTimeInterface
{
return $this->time;
}
/**
* @param DateTimeInterface $time
*/
public function setTime(DateTimeInterface $time): void
{
$this->time = $time;
}
/**
* @return int
*/
public function getSize(): int
{
return $this->size;
}
/**
* @param int $size
*/
public function setSize(int $size): void
{
$this->size = $size;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace ZipStream\Option;
use MyCLabs\Enum\Enum;
/**
* Methods enum
*
* @method static STORE(): Method
* @method static DEFLATE(): Method
* @psalm-immutable
* @psalm-template int
* @extends Enum<int>
*/
class Method extends Enum
{
public const STORE = 0x00;
public const DEFLATE = 0x08;
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace ZipStream\Option;
use MyCLabs\Enum\Enum;
/**
* Class Version
* @package ZipStream\Option
*
* @method static STORE(): Version
* @method static DEFLATE(): Version
* @method static ZIP64(): Version
* @psalm-immutable
* @psalm-template int
* @extends Enum<int>
*/
class Version extends Enum
{
public const STORE = 0x000A; // 1.00
public const DEFLATE = 0x0014; // 2.00
public const ZIP64 = 0x002D; // 4.50
}

View File

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use RuntimeException;
/**
* @internal
* TODO: Make class readonly when requiring PHP 8.2 exclusively
*/
class PackField
{
public const MAX_V = 0xFFFFFFFF;
public const MAX_v = 0xFFFF;
public function __construct(
public readonly string $format,
public readonly int|string $value
) {
}
/**
* Create a format string and argument list for pack(), then call
* pack() and return the result.
*/
public static function pack(self ...$fields): string
{
$fmt = array_reduce($fields, function (string $acc, self $field) {
return $acc . $field->format;
}, '');
$args = array_map(function (self $field) {
switch($field->format) {
case 'V':
if ($field->value > self::MAX_V) {
throw new RuntimeException(print_r($field->value, true) . ' is larger than 32 bits');
}
break;
case 'v':
if ($field->value > self::MAX_v) {
throw new RuntimeException(print_r($field->value, true) . ' is larger than 16 bits');
}
break;
case 'P': break;
default:
break;
}
return $field->value;
}, $fields);
return pack($fmt, ...$args);
}
}

View File

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

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use ZipStream\Exception\DosTimeOverflowException;
/**
* @internal
*/
abstract class Time
{
private const DOS_MINIMUM_DATE = '1980-01-01 00:00:00Z';
public static function dateTimeToDosTime(DateTimeInterface $dateTime): int
{
$dosMinimumDate = new DateTimeImmutable(self::DOS_MINIMUM_DATE);
if ($dateTime->getTimestamp() < $dosMinimumDate->getTimestamp()) {
throw new DosTimeOverflowException(dateTime: $dateTime);
}
$dateTime = DateTimeImmutable::createFromInterface($dateTime)->sub(new DateInterval('P1980Y'));
['year' => $year,
'mon' => $month,
'mday' => $day,
'hours' => $hour,
'minutes' => $minute,
'seconds' => $second
] = getdate($dateTime->getTimestamp());
return
($year << 25) |
($month << 21) |
($day << 16) |
($hour << 11) |
($minute << 5) |
($second >> 1);
}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream;
enum Version: int
{
case STORE = 0x000A; // 1.00
case DEFLATE = 0x0014; // 2.00
case ZIP64 = 0x002D; // 4.50
}

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Zip64;
use ZipStream\PackField;
/**
* @internal
*/
abstract class DataDescriptor
{
private const SIGNATURE = 0x08074b50;
public static function generate(
int $crc32UncompressedData,
int $compressedSize,
int $uncompressedSize,
): string {
return PackField::pack(
new PackField(format: 'V', value: self::SIGNATURE),
new PackField(format: 'V', value: $crc32UncompressedData),
new PackField(format: 'P', value: $compressedSize),
new PackField(format: 'P', value: $uncompressedSize),
);
}
}

View File

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Zip64;
use ZipStream\PackField;
/**
* @internal
*/
abstract class EndOfCentralDirectory
{
private const SIGNATURE = 0x06064b50;
public static function generate(
int $versionMadeBy,
int $versionNeededToExtract,
int $numberOfThisDisk,
int $numberOfTheDiskWithCentralDirectoryStart,
int $numberOfCentralDirectoryEntriesOnThisDisk,
int $numberOfCentralDirectoryEntries,
int $sizeOfCentralDirectory,
int $centralDirectoryStartOffsetOnDisk,
string $extensibleDataSector,
): string {
$recordSize = 44 + strlen($extensibleDataSector); // (length of block - 12) = 44;
/** @psalm-suppress MixedArgument */
return PackField::pack(
new PackField(format: 'V', value: static::SIGNATURE),
new PackField(format: 'P', value: $recordSize),
new PackField(format: 'v', value: $versionMadeBy),
new PackField(format: 'v', value: $versionNeededToExtract),
new PackField(format: 'V', value: $numberOfThisDisk),
new PackField(format: 'V', value: $numberOfTheDiskWithCentralDirectoryStart),
new PackField(format: 'P', value: $numberOfCentralDirectoryEntriesOnThisDisk),
new PackField(format: 'P', value: $numberOfCentralDirectoryEntries),
new PackField(format: 'P', value: $sizeOfCentralDirectory),
new PackField(format: 'P', value: $centralDirectoryStartOffsetOnDisk),
) . $extensibleDataSector;
}
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Zip64;
use ZipStream\PackField;
/**
* @internal
*/
abstract class EndOfCentralDirectoryLocator
{
private const SIGNATURE = 0x07064b50;
public static function generate(
int $numberOfTheDiskWithZip64CentralDirectoryStart,
int $zip64centralDirectoryStartOffsetOnDisk,
int $totalNumberOfDisks,
): string {
/** @psalm-suppress MixedArgument */
return PackField::pack(
new PackField(format: 'V', value: static::SIGNATURE),
new PackField(format: 'V', value: $numberOfTheDiskWithZip64CentralDirectoryStart),
new PackField(format: 'P', value: $zip64centralDirectoryStartOffsetOnDisk),
new PackField(format: 'V', value: $totalNumberOfDisks),
);
}
}

View File

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Zip64;
use ZipStream\PackField;
/**
* @internal
*/
abstract class ExtendedInformationExtraField
{
private const TAG = 0x0001;
public static function generate(
?int $originalSize = null,
?int $compressedSize = null,
?int $relativeHeaderOffset = null,
?int $diskStartNumber = null,
): string {
return PackField::pack(
new PackField(format: 'v', value: self::TAG),
new PackField(
format: 'v',
value:
($originalSize === null ? 0 : 8) +
($compressedSize === null ? 0 : 8) +
($relativeHeaderOffset === null ? 0 : 8) +
($diskStartNumber === null ? 0 : 4)
),
...($originalSize === null ? [] : [
new PackField(format: 'P', value: $originalSize),
]),
...($compressedSize === null ? [] : [
new PackField(format: 'P', value: $compressedSize),
]),
...($relativeHeaderOffset === null ? [] : [
new PackField(format: 'P', value: $relativeHeaderOffset),
]),
...($diskStartNumber === null ? [] : [
new PackField(format: 'V', value: $diskStartNumber),
]),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Zs;
use ZipStream\PackField;
/**
* @internal
*/
abstract class ExtendedInformationExtraField
{
private const TAG = 0x5653;
public static function generate(): string
{
return PackField::pack(
new PackField(format: 'v', value: self::TAG),
new PackField(format: 'v', value: 0x0000),
);
}
}

View File

@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
trait Assertions
{
protected function assertFileContains(string $filePath, string $needle): void
{
$last = '';
$handle = fopen($filePath, 'r');
while (!feof($handle)) {
$line = fgets($handle, 1024);
if(str_contains($last . $line, $needle)) {
fclose($handle);
return;
}
$last = $line;
}
fclose($handle);
$this->fail("File {$filePath} must contain {$needle}");
}
protected function assertFileDoesNotContain(string $filePath, string $needle): void
{
$last = '';
$handle = fopen($filePath, 'r');
while (!feof($handle)) {
$line = fgets($handle, 1024);
if(str_contains($last . $line, $needle)) {
fclose($handle);
$this->fail("File {$filePath} must not contain {$needle}");
}
$last = $line;
}
fclose($handle);
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace BigintTest;
use OverflowException;
use PHPUnit\Framework\TestCase;
use ZipStream\Bigint;
class BigintTest extends TestCase
{
public function testConstruct(): void
{
$bigint = new Bigint(0x12345678);
$this->assertSame('0x0000000012345678', $bigint->getHex64());
$this->assertSame(0x12345678, $bigint->getLow32());
$this->assertSame(0, $bigint->getHigh32());
}
public function testConstructLarge(): void
{
$bigint = new Bigint(0x87654321);
$this->assertSame('0x0000000087654321', $bigint->getHex64());
$this->assertSame('87654321', bin2hex(pack('N', $bigint->getLow32())));
$this->assertSame(0, $bigint->getHigh32());
}
public function testAddSmallValue(): void
{
$bigint = new Bigint(1);
$bigint = $bigint->add(Bigint::init(2));
$this->assertSame(3, $bigint->getLow32());
$this->assertFalse($bigint->isOver32());
$this->assertTrue($bigint->isOver32(true));
$this->assertSame($bigint->getLowFF(), (float)$bigint->getLow32());
$this->assertSame($bigint->getLowFF(true), (float)0xFFFFFFFF);
}
public function testAddWithOverflowAtLowestByte(): void
{
$bigint = new Bigint(0xFF);
$bigint = $bigint->add(Bigint::init(0x01));
$this->assertSame(0x100, $bigint->getLow32());
}
public function testAddWithOverflowAtInteger32(): void
{
$bigint = new Bigint(0xFFFFFFFE);
$this->assertFalse($bigint->isOver32());
$bigint = $bigint->add(Bigint::init(0x01));
$this->assertTrue($bigint->isOver32());
$bigint = $bigint->add(Bigint::init(0x01));
$this->assertSame('0x0000000100000000', $bigint->getHex64());
$this->assertTrue($bigint->isOver32());
$this->assertSame((float)0xFFFFFFFF, $bigint->getLowFF());
}
public function testAddWithOverflowAtInteger64(): void
{
$bigint = Bigint::fromLowHigh(0xFFFFFFFF, 0xFFFFFFFF);
$this->assertSame('0xFFFFFFFFFFFFFFFF', $bigint->getHex64());
$this->expectException(OverflowException::class);
$bigint->add(Bigint::init(1));
}
}

View File

@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\CentralDirectoryFileHeader;
use ZipStream\CompressionMethod;
class CentralDirectoryFileHeaderTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z');
$header = CentralDirectoryFileHeader::generate(
versionMadeBy: 0x603,
versionNeededToExtract: 0x002D,
generalPurposeBitFlag: 0x2222,
compressionMethod: CompressionMethod::DEFLATE,
lastModificationDateTime: $dateTime,
crc32: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
fileName: 'test.png',
extraField: 'some content',
fileComment: 'some comment',
diskNumberStart: 0,
internalFileAttributes: 0,
externalFileAttributes: 32,
relativeOffsetOfLocalHeader: 0x1234,
);
$this->assertSame(
bin2hex($header),
'504b0102' . // 4 bytes; central file header signature
'0306' . // 2 bytes; version made by
'2d00' . // 2 bytes; version needed to extract
'2222' . // 2 bytes; general purpose bit flag
'0800' . // 2 bytes; compression method
'2008' . // 2 bytes; last mod file time
'2154' . // 2 bytes; last mod file date
'11111111' . // 4 bytes; crc-32
'77777777' . // 4 bytes; compressed size
'99999999' . // 4 bytes; uncompressed size
'0800' . // 2 bytes; file name length (n)
'0c00' . // 2 bytes; extra field length (m)
'0c00' . // 2 bytes; file comment length (o)
'0000' . // 2 bytes; disk number start
'0000' . // 2 bytes; internal file attributes
'20000000' . // 4 bytes; external file attributes
'34120000' . // 4 bytes; relative offset of local header
'746573742e706e67' . // n bytes; file name
'736f6d6520636f6e74656e74' . // m bytes; extra field
'736f6d6520636f6d6d656e74' // o bytes; file comment
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use ZipStream\DataDescriptor;
class DataDescriptorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$this->assertSame(
bin2hex(DataDescriptor::generate(
crc32UncompressedData: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
)),
'504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'77777777' . // 4 bytes; Compressed size
'99999999' // 4 bytes; Uncompressed size
);
}
}

View File

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use ZipStream\EndOfCentralDirectory;
class EndOfCentralDirectoryTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$this->assertSame(
bin2hex(EndOfCentralDirectory::generate(
numberOfThisDisk: 0x00,
numberOfTheDiskWithCentralDirectoryStart: 0x00,
numberOfCentralDirectoryEntriesOnThisDisk: 0x10,
numberOfCentralDirectoryEntries: 0x10,
sizeOfCentralDirectory: 0x22,
centralDirectoryStartOffsetOnDisk: 0x33,
zipFileComment: 'foo',
)),
'504b0506' . // 4 bytes; end of central dir signature 0x06054b50
'0000' . // 2 bytes; number of this disk
'0000' . // 2 bytes; number of the disk with the start of the central directory
'1000' . // 2 bytes; total number of entries in the central directory on this disk
'1000' . // 2 bytes; total number of entries in the central directory
'22000000' . // 4 bytes; size of the central directory
'33000000' . // 4 bytes; offset of start of central directory with respect to the starting disk number
'0300' . // 2 bytes; .ZIP file comment length
bin2hex('foo')
);
}
}

View File

@ -1,106 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class EndlessCycleStream implements StreamInterface
{
private int $offset = 0;
public function __construct(private readonly string $toRepeat = '0')
{
}
public function __toString(): string
{
throw new RuntimeException('Infinite Stream!');
}
public function close(): void
{
$this->detach();
}
/**
* @return null
*/
public function detach()
{
return;
}
public function getSize(): ?int
{
return null;
}
public function tell(): int
{
return $this->offset;
}
public function eof(): bool
{
return false;
}
public function isSeekable(): bool
{
return true;
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
switch($whence) {
case SEEK_SET:
$this->offset = $offset;
break;
case SEEK_CUR:
$this->offset += $offset;
break;
case SEEK_END:
throw new RuntimeException('Infinite Stream!');
break;
}
}
public function rewind(): void
{
$this->seek(0);
}
public function isWritable(): bool
{
return false;
}
public function write(string $string): int
{
throw new RuntimeException('Not writeable');
}
public function isReadable(): bool
{
return true;
}
public function read(int $length): string
{
$this->offset += $length;
return substr(str_repeat($this->toRepeat, (int) ceil($length / strlen($this->toRepeat))), 0, $length);
}
public function getContents(): string
{
throw new RuntimeException('Infinite Stream!');
}
public function getMetadata(?string $key = null): array|null
{
return $key !== null ? null : [];
}
}

View File

@ -1,141 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
class FaultInjectionResource
{
public const NAME = 'zipstream-php-test-broken-resource';
/** @var resource */
public $context;
private array $injectFaults;
private string $mode;
/**
* @return resource
*/
public static function getResource(array $injectFaults)
{
self::register();
return fopen(self::NAME . '://foobar', 'rw+', false, self::createStreamContext($injectFaults));
}
public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool
{
$options = stream_context_get_options($this->context);
if (!isset($options[self::NAME]['injectFaults'])) {
return false;
}
$this->mode = $mode;
$this->injectFaults = $options[self::NAME]['injectFaults'];
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_write(string $data)
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_eof()
{
return true;
}
public function stream_seek(int $offset, int $whence): bool
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return true;
}
public function stream_tell(): int
{
if ($this->shouldFail(__FUNCTION__)) {
return false;
}
return 0;
}
public static function register(): void
{
if (!in_array(self::NAME, stream_get_wrappers(), true)) {
stream_wrapper_register(self::NAME, __CLASS__);
}
}
public function stream_stat(): array
{
static $modeMap = [
'r' => 33060,
'rb' => 33060,
'r+' => 33206,
'w' => 33188,
'wb' => 33188,
];
return [
'dev' => 0,
'ino' => 0,
'mode' => $modeMap[$this->mode],
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
public function url_stat(string $path, int $flags): array
{
return [
'dev' => 0,
'ino' => 0,
'mode' => 0,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
private static function createStreamContext(array $injectFaults)
{
return stream_context_create([
self::NAME => ['injectFaults' => $injectFaults],
]);
}
private function shouldFail(string $function): bool
{
return in_array($function, $this->injectFaults, true);
}
}

View File

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\CompressionMethod;
use ZipStream\LocalFileHeader;
class LocalFileHeaderTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z');
$header = LocalFileHeader::generate(
versionNeededToExtract: 0x002D,
generalPurposeBitFlag: 0x2222,
compressionMethod: CompressionMethod::DEFLATE,
lastModificationDateTime: $dateTime,
crc32UncompressedData: 0x11111111,
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
fileName: 'test.png',
extraField: 'some content'
);
$this->assertSame(
bin2hex((string) $header),
'504b0304' . // 4 bytes; Local file header signature
'2d00' . // 2 bytes; Version needed to extract (minimum)
'2222' . // 2 bytes; General purpose bit flag
'0800' . // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8
'2008' . // 2 bytes; File last modification time
'2154' . // 2 bytes; File last modification date
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'77777777' . // 4 bytes; Compressed size (or 0xffffffff for ZIP64)
'99999999' . // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64)
'0800' . // 2 bytes; File name length (n)
'0c00' . // 2 bytes; Extra field length (m)
'746573742e706e67' . // n bytes; File name
'736f6d6520636f6e74656e74' // m bytes; Extra field
);
}
}

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use ZipStream\PackField;
class PackFieldTest extends TestCase
{
public function testPacksFields(): void
{
$this->assertSame(
bin2hex(PackField::pack(new PackField(format: 'v', value: 0x1122))),
'2211',
);
}
public function testOverflow2(): void
{
$this->expectException(RuntimeException::class);
PackField::pack(new PackField(format: 'v', value: 0xFFFFF));
}
public function testOverflow4(): void
{
$this->expectException(RuntimeException::class);
PackField::pack(new PackField(format: 'V', value: 0xFFFFFFFFF));
}
public function testUnknownOperator(): void
{
$this->assertSame(
bin2hex(PackField::pack(new PackField(format: 'a', value: 0x1122))),
'34',
);
}
}

View File

@ -1,160 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* @internal
*/
class ResourceStream implements StreamInterface
{
public function __construct(
/**
* @var resource
*/
private $stream
) {
}
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return (string) stream_get_contents($this->stream);
}
public function close(): void
{
$stream = $this->detach();
if ($stream) {
fclose($stream);
}
}
public function detach()
{
$result = $this->stream;
// According to the interface, the stream is left in an unusable state;
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->stream = null;
return $result;
}
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!$this->isSeekable()) {
throw new RuntimeException();
}
if (fseek($this->stream, $offset, $whence) !== 0) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
}
public function isSeekable(): bool
{
return (bool)$this->getMetadata('seekable');
}
public function getMetadata(?string $key = null)
{
$metadata = stream_get_meta_data($this->stream);
return $key !== null ? @$metadata[$key] : $metadata;
}
public function getSize(): ?int
{
$stats = fstat($this->stream);
return $stats['size'];
}
public function tell(): int
{
$position = ftell($this->stream);
if ($position === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $position;
}
public function eof(): bool
{
return feof($this->stream);
}
public function rewind(): void
{
$this->seek(0);
}
public function write(string $string): int
{
if (!$this->isWritable()) {
throw new RuntimeException();
}
if (fwrite($this->stream, $string) === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return strlen($string);
}
public function isWritable(): bool
{
$mode = $this->getMetadata('mode');
if (!is_string($mode)) {
// @codeCoverageIgnoreStart
throw new RuntimeException('Could not get stream mode from metadata!');
// @codeCoverageIgnoreEnd
}
return preg_match('/[waxc+]/', $mode) === 1;
}
public function read(int $length): string
{
if (!$this->isReadable()) {
throw new RuntimeException();
}
$result = fread($this->stream, $length);
if ($result === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $result;
}
public function isReadable(): bool
{
$mode = $this->getMetadata('mode');
if (!is_string($mode)) {
// @codeCoverageIgnoreStart
throw new RuntimeException('Could not get stream mode from metadata!');
// @codeCoverageIgnoreEnd
}
return preg_match('/[r+]/', $mode) === 1;
}
public function getContents(): string
{
if (!$this->isReadable()) {
throw new RuntimeException();
}
$result = stream_get_contents($this->stream);
if ($result === false) {
// @codeCoverageIgnoreStart
throw new RuntimeException();
// @codeCoverageIgnoreEnd
}
return $result;
}
}

View File

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ZipStream\Exception\DosTimeOverflowException;
use ZipStream\Time;
class TimeTest extends TestCase
{
public function testNormalDateToDosTime(): void
{
$this->assertSame(
Time::dateTimeToDosTime(new DateTimeImmutable('2014-11-17T17:46:08Z')),
1165069764
);
// January 1 1980 - DOS Epoch.
$this->assertSame(
Time::dateTimeToDosTime(new DateTimeImmutable('1980-01-01T00:00:00+00:00')),
2162688
);
}
public function testTooEarlyDateToDosTime(): void
{
$this->expectException(DosTimeOverflowException::class);
// January 1 1980 is the minimum DOS Epoch.
Time::dateTimeToDosTime(new DateTimeImmutable('1970-01-01T00:00:00+00:00'));
}
}

View File

@ -1,135 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test;
use function fgets;
use function pclose;
use function popen;
use function preg_match;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function strtolower;
use ZipArchive;
trait Util
{
protected function getTmpFileStream(): array
{
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
$stream = fopen($tmp, 'wb+');
return [$tmp, $stream];
}
protected function cmdExists(string $command): bool
{
if (strtolower(\substr(PHP_OS, 0, 3)) === 'win') {
$fp = popen("where $command", 'r');
$result = fgets($fp, 255);
$exists = !preg_match('#Could not find files#', $result);
pclose($fp);
} else { // non-Windows
$fp = popen("which $command", 'r');
$result = fgets($fp, 255);
$exists = !empty($result);
pclose($fp);
}
return $exists;
}
protected function dumpZipContents(string $path): string
{
if (!$this->cmdExists('hexdump')) {
return '';
}
$output = [];
if (!exec("hexdump -C \"$path\" | head -n 50", $output)) {
return '';
}
return "\nHexdump:\n" . implode("\n", $output);
}
protected function validateAndExtractZip(string $zipPath): string
{
$tmpDir = $this->getTmpDir();
$zipArchive = new ZipArchive();
$result = $zipArchive->open($zipPath);
if ($result !== true) {
$codeName = $this->zipArchiveOpenErrorCodeName($result);
$debugInformation = $this->dumpZipContents($zipPath);
$this->fail("Failed to open {$zipPath}. Code: $result ($codeName)$debugInformation");
return $tmpDir;
}
$this->assertSame(0, $zipArchive->status);
$this->assertSame(0, $zipArchive->statusSys);
$zipArchive->extractTo($tmpDir);
$zipArchive->close();
return $tmpDir;
}
protected function zipArchiveOpenErrorCodeName(int $code): string
{
switch($code) {
case ZipArchive::ER_EXISTS: return 'ER_EXISTS';
case ZipArchive::ER_INCONS: return 'ER_INCONS';
case ZipArchive::ER_INVAL: return 'ER_INVAL';
case ZipArchive::ER_MEMORY: return 'ER_MEMORY';
case ZipArchive::ER_NOENT: return 'ER_NOENT';
case ZipArchive::ER_NOZIP: return 'ER_NOZIP';
case ZipArchive::ER_OPEN: return 'ER_OPEN';
case ZipArchive::ER_READ: return 'ER_READ';
case ZipArchive::ER_SEEK: return 'ER_SEEK';
default: return 'unknown';
}
}
protected function getTmpDir(): string
{
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
unlink($tmp);
mkdir($tmp) or $this->fail('Failed to make directory');
return $tmp;
}
/**
* @return string[]
*/
protected function getRecursiveFileList(string $path, bool $includeDirectories = false): array
{
$data = [];
$path = (string)realpath($path);
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
$pathLen = strlen($path);
foreach ($files as $file) {
$filePath = $file->getRealPath();
if (is_dir($filePath) && !$includeDirectories) {
continue;
}
$data[] = substr($filePath, $pathLen + 1);
}
sort($data);
return $data;
}
}

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\DataDescriptor;
class DataDescriptorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$descriptor = DataDescriptor::generate(
crc32UncompressedData: 0x11111111,
compressedSize: (0x77777777 << 32) + 0x66666666,
uncompressedSize: (0x99999999 << 32) + 0x88888888,
);
$this->assertSame(
bin2hex($descriptor),
'504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
'11111111' . // 4 bytes; CRC-32 of uncompressed data
'6666666677777777' . // 8 bytes; Compressed size
'8888888899999999' // 8 bytes; Uncompressed size
);
}
}

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace ZipStream\Test\Zip64;
use PHPUnit\Framework\TestCase;
use ZipStream\Zip64\EndOfCentralDirectoryLocator;
class EndOfCentralDirectoryLocatorTest extends TestCase
{
public function testSerializesCorrectly(): void
{
$descriptor = EndOfCentralDirectoryLocator::generate(
numberOfTheDiskWithZip64CentralDirectoryStart: 0x11111111,
zip64centralDirectoryStartOffsetOnDisk: (0x22222222 << 32) + 0x33333333,
totalNumberOfDisks: 0x44444444,
);
$this->assertSame(
bin2hex($descriptor),
'504b0607' . // 4 bytes; zip64 end of central dir locator signature - 0x07064b50
'11111111' . // 4 bytes; number of the disk with the start of the zip64 end of central directory
'3333333322222222' . // 28 bytes; relative offset of the zip64 end of central directory record
'44444444' // 4 bytes;total number of disks
);
}
}

Some files were not shown because too many files have changed in this diff Show More