added composer files and installed vendor packages. Also modified xsls to display base project
This commit is contained in:
+1
-1
@@ -289,7 +289,7 @@ class Alerts extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -583,7 +583,7 @@ class Base {
|
|||||||
public function getZipcode($zip_code = 0) {
|
public function getZipcode($zip_code = 0) {
|
||||||
|
|
||||||
$zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
$zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
||||||
$zip_code = (integer) substr((string) $zip_code, 0, 5);
|
$zip_code = (int) substr((string) $zip_code, 0, 5);
|
||||||
|
|
||||||
$zipCodeDetail = null;
|
$zipCodeDetail = null;
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ class Dropdowns extends Base {
|
|||||||
|
|
||||||
public function dropdowntypeActive() {
|
public function dropdowntypeActive() {
|
||||||
|
|
||||||
$dropdowntype_serial = (integer) filter_input(INPUT_POST, "dropdowntype_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
$dropdowntype_serial = (int) filter_input(INPUT_POST, "dropdowntype_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
||||||
|
|
||||||
$SQL = "select dropdown_serial
|
$SQL = "select dropdown_serial
|
||||||
from view_dropdowns
|
from view_dropdowns
|
||||||
@@ -587,7 +587,7 @@ class Dropdowns extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ class Journal extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class Popovers extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -215,7 +215,7 @@ class Reports extends Base {
|
|||||||
|
|
||||||
$reports = $this->getTable("reports", $SQL);
|
$reports = $this->getTable("reports", $SQL);
|
||||||
|
|
||||||
return (integer) ($reports->record->report_serial ?? 0);
|
return (int) ($reports->record->report_serial ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =======================
|
// =======================
|
||||||
@@ -268,7 +268,7 @@ class Reports extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ class Reports extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ class SignIn extends Base {
|
|||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((integer) $user->record->user_active === 0) {
|
if ((int) $user->record->user_active === 0) {
|
||||||
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
|
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ class SignIn extends Base {
|
|||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
|
|
||||||
$_SESSION[self::USER_AUTHENTICATED] = true;
|
$_SESSION[self::USER_AUTHENTICATED] = true;
|
||||||
$_SESSION[self::USER_SERIAL] = (integer) $user->record->user_serial;
|
$_SESSION[self::USER_SERIAL] = (int) $user->record->user_serial;
|
||||||
$_SESSION[self::USER_ID] = (string) $user->record->user_id;
|
$_SESSION[self::USER_ID] = (string) $user->record->user_id;
|
||||||
$_SESSION[self::USER_NAME] = (string) $user->record->user_name;
|
$_SESSION[self::USER_NAME] = (string) $user->record->user_name;
|
||||||
$_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
|
$_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ class Statuses extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -111,8 +111,8 @@ class Users extends Base {
|
|||||||
$user_random_password = $this->generateRandomPassword();
|
$user_random_password = $this->generateRandomPassword();
|
||||||
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
|
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
$user_change_password = (boolean) true;
|
$user_change_password = (int) true;
|
||||||
$user_active = (boolean) true;
|
$user_active = (int) true;
|
||||||
|
|
||||||
// Add the User
|
// Add the User
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ class Users extends Base {
|
|||||||
|
|
||||||
case "prompt":
|
case "prompt":
|
||||||
|
|
||||||
$user_serial = (integer) filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
$user_serial = (int) filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
||||||
|
|
||||||
$user = $this->getUser($user_serial);
|
$user = $this->getUser($user_serial);
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ class Users extends Base {
|
|||||||
|
|
||||||
$users = $this->getUserByUserId($user_id);
|
$users = $this->getUserByUserId($user_id);
|
||||||
|
|
||||||
return ($users->count() === 0) ? 0 : (integer) $users->record->user_serial;
|
return ($users->count() === 0) ? 0 : (int) $users->record->user_serial;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -354,7 +354,7 @@ class Users extends Base {
|
|||||||
|
|
||||||
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
|
$count = (count($recordset) > 0) ? (int) $recordset["count"] : (int) 0;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ class Users extends Base {
|
|||||||
|
|
||||||
public function userActive() {
|
public function userActive() {
|
||||||
|
|
||||||
$user_serial = (integer) filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
$user_serial = (int) filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
|
||||||
|
|
||||||
$SQL = "select request_serial
|
$SQL = "select request_serial
|
||||||
from requests
|
from requests
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 862 KiB |
@@ -32,7 +32,6 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
define("INITIALIZATION_FILE_NAME", "../xml/settings.xml");
|
define("INITIALIZATION_FILE_NAME", "../xml/settings.xml");
|
||||||
define("INIT_SCRIPT_USER", "Init Script");
|
define("INIT_SCRIPT_USER", "Init Script");
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ try {
|
|||||||
);
|
);
|
||||||
|
|
||||||
$connection->exec("SET FOREIGN_KEY_CHECKS = 0");
|
$connection->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||||
|
|
||||||
} catch (PDOException | Exception $e) {
|
} catch (PDOException | Exception $e) {
|
||||||
echoLog("Unable to connect to the database.", "error");
|
echoLog("Unable to connect to the database.", "error");
|
||||||
exit();
|
exit();
|
||||||
@@ -304,7 +302,6 @@ function echoLog($message, $type = "") {
|
|||||||
$class = trim("log {$type}");
|
$class = trim("log {$type}");
|
||||||
echo "<div class='{$class}'>" . htmlspecialchars($message, ENT_QUOTES, "UTF-8") . "</div>";
|
echo "<div class='{$class}'>" . htmlspecialchars($message, ENT_QUOTES, "UTF-8") . "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, $err);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException($err);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit6621356a0da1644f04f6f40bd0e0b948::getLoader();
|
||||||
Vendored
+579
@@ -0,0 +1,579 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||||
|
*
|
||||||
|
* $loader = new \Composer\Autoload\ClassLoader();
|
||||||
|
*
|
||||||
|
* // register classes with namespaces
|
||||||
|
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||||
|
* $loader->add('Symfony', __DIR__.'/framework');
|
||||||
|
*
|
||||||
|
* // activate the autoloader
|
||||||
|
* $loader->register();
|
||||||
|
*
|
||||||
|
* // to enable searching the include path (eg. for PEAR packages)
|
||||||
|
* $loader->setUseIncludePath(true);
|
||||||
|
*
|
||||||
|
* In this example, if you try to use a class in the Symfony\Component
|
||||||
|
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||||
|
* the autoloader will first look for the class under the component/
|
||||||
|
* directory, and it will then fallback to the framework/ directory if not
|
||||||
|
* found before giving up.
|
||||||
|
*
|
||||||
|
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
* @see https://www.php-fig.org/psr/psr-0/
|
||||||
|
* @see https://www.php-fig.org/psr/psr-4/
|
||||||
|
*/
|
||||||
|
class ClassLoader
|
||||||
|
{
|
||||||
|
/** @var \Closure(string):void */
|
||||||
|
private static $includeFile;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
|
// PSR-4
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
private $prefixLengthsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private $prefixDirsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr4 = array();
|
||||||
|
|
||||||
|
// PSR-0
|
||||||
|
/**
|
||||||
|
* List of PSR-0 prefixes
|
||||||
|
*
|
||||||
|
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||||
|
*
|
||||||
|
* @var array<string, array<string, list<string>>>
|
||||||
|
*/
|
||||||
|
private $prefixesPsr0 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr0 = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $useIncludePath = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private $classMap = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $classMapAuthoritative = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>
|
||||||
|
*/
|
||||||
|
private $missingClasses = array();
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $apcuPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, self>
|
||||||
|
*/
|
||||||
|
private static $registeredLoaders = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $vendorDir
|
||||||
|
*/
|
||||||
|
public function __construct($vendorDir = null)
|
||||||
|
{
|
||||||
|
$this->vendorDir = $vendorDir;
|
||||||
|
self::initializeIncludeClosure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixes()
|
||||||
|
{
|
||||||
|
if (!empty($this->prefixesPsr0)) {
|
||||||
|
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixesPsr4()
|
||||||
|
{
|
||||||
|
return $this->prefixDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirs()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirsPsr4()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> Array of classname => path
|
||||||
|
*/
|
||||||
|
public function getClassMap()
|
||||||
|
{
|
||||||
|
return $this->classMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $classMap Class to filename map
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addClassMap(array $classMap)
|
||||||
|
{
|
||||||
|
if ($this->classMap) {
|
||||||
|
$this->classMap = array_merge($this->classMap, $classMap);
|
||||||
|
} else {
|
||||||
|
$this->classMap = $classMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix, either
|
||||||
|
* appending or prepending to the ones previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 root directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$this->fallbackDirsPsr0,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $prefix[0];
|
||||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($prepend) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixesPsr0[$first][$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$this->prefixesPsr0[$first][$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace, either
|
||||||
|
* appending or prepending to the ones previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addPsr4($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
// Register directories for the root namespace.
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr4
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$this->fallbackDirsPsr4,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||||
|
// Register directories for a new namespace.
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||||
|
} elseif ($prepend) {
|
||||||
|
// Prepend directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixDirsPsr4[$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Append directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$this->prefixDirsPsr4[$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix,
|
||||||
|
* replacing any others previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 base directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr0 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace,
|
||||||
|
* replacing any others previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPsr4($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr4 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on searching the include path for class files.
|
||||||
|
*
|
||||||
|
* @param bool $useIncludePath
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUseIncludePath($useIncludePath)
|
||||||
|
{
|
||||||
|
$this->useIncludePath = $useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to check if the autoloader uses the include path to check
|
||||||
|
* for classes.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getUseIncludePath()
|
||||||
|
{
|
||||||
|
return $this->useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off searching the prefix and fallback directories for classes
|
||||||
|
* that have not been registered with the class map.
|
||||||
|
*
|
||||||
|
* @param bool $classMapAuthoritative
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||||
|
{
|
||||||
|
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should class lookup fail if not found in the current class map?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isClassMapAuthoritative()
|
||||||
|
{
|
||||||
|
return $this->classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||||
|
*
|
||||||
|
* @param string|null $apcuPrefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setApcuPrefix($apcuPrefix)
|
||||||
|
{
|
||||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getApcuPrefix()
|
||||||
|
{
|
||||||
|
return $this->apcuPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @param bool $prepend Whether to prepend the autoloader or not
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register($prepend = false)
|
||||||
|
{
|
||||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||||
|
|
||||||
|
if (null === $this->vendorDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prepend) {
|
||||||
|
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||||
|
} else {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unregister()
|
||||||
|
{
|
||||||
|
spl_autoload_unregister(array($this, 'loadClass'));
|
||||||
|
|
||||||
|
if (null !== $this->vendorDir) {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given class or interface.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
* @return true|null True if loaded, null otherwise
|
||||||
|
*/
|
||||||
|
public function loadClass($class)
|
||||||
|
{
|
||||||
|
if ($file = $this->findFile($class)) {
|
||||||
|
$includeFile = self::$includeFile;
|
||||||
|
$includeFile($file);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the file where the class is defined.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
*
|
||||||
|
* @return string|false The path if found, false otherwise
|
||||||
|
*/
|
||||||
|
public function findFile($class)
|
||||||
|
{
|
||||||
|
// class map lookup
|
||||||
|
if (isset($this->classMap[$class])) {
|
||||||
|
return $this->classMap[$class];
|
||||||
|
}
|
||||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||||
|
if ($hit) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->findFileWithExtension($class, '.php');
|
||||||
|
|
||||||
|
// Search for Hack files if we are running on HHVM
|
||||||
|
if (false === $file && defined('HHVM_VERSION')) {
|
||||||
|
$file = $this->findFileWithExtension($class, '.hh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
apcu_add($this->apcuPrefix.$class, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $file) {
|
||||||
|
// Remember that this class does not exist.
|
||||||
|
$this->missingClasses[$class] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||||
|
*
|
||||||
|
* @return array<string, self>
|
||||||
|
*/
|
||||||
|
public static function getRegisteredLoaders()
|
||||||
|
{
|
||||||
|
return self::$registeredLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $ext
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function findFileWithExtension($class, $ext)
|
||||||
|
{
|
||||||
|
// PSR-4 lookup
|
||||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
|
||||||
|
$first = $class[0];
|
||||||
|
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||||
|
$subPath = $class;
|
||||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||||
|
$subPath = substr($subPath, 0, $lastPos);
|
||||||
|
$search = $subPath . '\\';
|
||||||
|
if (isset($this->prefixDirsPsr4[$search])) {
|
||||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||||
|
if (file_exists($file = $dir . $pathEnd)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-4 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 lookup
|
||||||
|
if (false !== $pos = strrpos($class, '\\')) {
|
||||||
|
// namespaced class name
|
||||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||||
|
} else {
|
||||||
|
// PEAR-like class name
|
||||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->prefixesPsr0[$first])) {
|
||||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||||
|
if (0 === strpos($class, $prefix)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 include paths.
|
||||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function initializeIncludeClosure()
|
||||||
|
{
|
||||||
|
if (self::$includeFile !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope isolated include.
|
||||||
|
*
|
||||||
|
* Prevents access to $this/self from included files.
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
self::$includeFile = \Closure::bind(static function($file) {
|
||||||
|
include $file;
|
||||||
|
}, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
|
use Composer\Semver\VersionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is copied in every Composer installed project and available to all
|
||||||
|
*
|
||||||
|
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||||
|
*
|
||||||
|
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class InstalledVersions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static $selfDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed[]|null
|
||||||
|
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||||
|
*/
|
||||||
|
private static $installed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $installedIsLocalDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
private static $canGetVendors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static $installedByVendor = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackages()
|
||||||
|
{
|
||||||
|
$packages = array();
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
$packages[] = array_keys($installed['versions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($packages)) {
|
||||||
|
return $packages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names with a specific type e.g. 'library'
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackagesByType($type)
|
||||||
|
{
|
||||||
|
$packagesByType = array();
|
||||||
|
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
foreach ($installed['versions'] as $name => $package) {
|
||||||
|
if (isset($package['type']) && $package['type'] === $type) {
|
||||||
|
$packagesByType[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packagesByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package is installed
|
||||||
|
*
|
||||||
|
* This also returns true if the package name is provided or replaced by another package
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @param bool $includeDevRequirements
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (isset($installed['versions'][$packageName])) {
|
||||||
|
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package satisfies a version constraint
|
||||||
|
*
|
||||||
|
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||||
|
*
|
||||||
|
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||||
|
*
|
||||||
|
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||||
|
* @param string $packageName
|
||||||
|
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||||
|
{
|
||||||
|
$constraint = $parser->parseConstraints((string) $constraint);
|
||||||
|
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||||
|
|
||||||
|
return $provided->matches($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||||
|
*
|
||||||
|
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||||
|
* whether a given version of a package is installed, and not just whether it exists
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string Version constraint usable with composer/semver
|
||||||
|
*/
|
||||||
|
public static function getVersionRanges($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = array();
|
||||||
|
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' || ', $ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getPrettyVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||||
|
*/
|
||||||
|
public static function getReference($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['reference'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||||
|
*/
|
||||||
|
public static function getInstallPath($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||||
|
*/
|
||||||
|
public static function getRootPackage()
|
||||||
|
{
|
||||||
|
$installed = self::getInstalled();
|
||||||
|
|
||||||
|
return $installed[0]['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw installed.php data for custom implementations
|
||||||
|
*
|
||||||
|
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||||
|
*/
|
||||||
|
public static function getRawData()
|
||||||
|
{
|
||||||
|
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
|
||||||
|
self::$installed = include __DIR__ . '/installed.php';
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||||
|
*
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
public static function getAllRawData()
|
||||||
|
{
|
||||||
|
return self::getInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you reload the static array from another file
|
||||||
|
*
|
||||||
|
* This is only useful for complex integrations in which a project needs to use
|
||||||
|
* this class but then also needs to execute another project's autoloader in process,
|
||||||
|
* and wants to ensure both projects have access to their version of installed.php.
|
||||||
|
*
|
||||||
|
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||||
|
* the data it needs from this class, then call reload() with
|
||||||
|
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||||
|
* the project in which it runs can then also use this class safely, without
|
||||||
|
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||||
|
*
|
||||||
|
* @param array[] $data A vendor/composer/installed.php data set
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||||
|
*/
|
||||||
|
public static function reload($data)
|
||||||
|
{
|
||||||
|
self::$installed = $data;
|
||||||
|
self::$installedByVendor = array();
|
||||||
|
|
||||||
|
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||||
|
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||||
|
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||||
|
// all installed packages for example
|
||||||
|
self::$installedIsLocalDir = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getSelfDir()
|
||||||
|
{
|
||||||
|
if (self::$selfDir === null) {
|
||||||
|
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$selfDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static function getInstalled()
|
||||||
|
{
|
||||||
|
if (null === self::$canGetVendors) {
|
||||||
|
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = array();
|
||||||
|
$copiedLocalDir = false;
|
||||||
|
|
||||||
|
if (self::$canGetVendors) {
|
||||||
|
$selfDir = self::getSelfDir();
|
||||||
|
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||||
|
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||||
|
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||||
|
$installed[] = self::$installedByVendor[$vendorDir];
|
||||||
|
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require $vendorDir.'/composer/installed.php';
|
||||||
|
self::$installedByVendor[$vendorDir] = $required;
|
||||||
|
$installed[] = $required;
|
||||||
|
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
self::$installed = $required;
|
||||||
|
self::$installedIsLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
$copiedLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require __DIR__ . '/installed.php';
|
||||||
|
self::$installed = $required;
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||||
|
$installed[] = self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_classmap.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||||
|
'Datamatrix' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
|
||||||
|
'PDF417' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/pdf417.php',
|
||||||
|
'QRcode' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/qrcode.php',
|
||||||
|
'TCPDF' => $vendorDir . '/tecnickcom/tcpdf/tcpdf.php',
|
||||||
|
'TCPDF2DBarcode' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_barcodes_2d.php',
|
||||||
|
'TCPDFBarcode' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_barcodes_1d.php',
|
||||||
|
'TCPDF_COLORS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_colors.php',
|
||||||
|
'TCPDF_FILTERS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_filters.php',
|
||||||
|
'TCPDF_FONTS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_fonts.php',
|
||||||
|
'TCPDF_FONT_DATA' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_font_data.php',
|
||||||
|
'TCPDF_IMAGES' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_images.php',
|
||||||
|
'TCPDF_STATIC' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_static.php',
|
||||||
|
);
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_namespaces.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
);
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_psr4.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
|
||||||
|
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
|
||||||
|
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
|
||||||
|
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
|
||||||
|
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
|
||||||
|
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
|
||||||
|
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
|
||||||
|
'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
|
||||||
|
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
|
||||||
|
'Com\\Tecnick\\Unicode\\Data\\' => array($vendorDir . '/tecnickcom/tc-lib-unicode-data/src'),
|
||||||
|
'Com\\Tecnick\\Unicode\\' => array($vendorDir . '/tecnickcom/tc-lib-unicode/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Sign\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-sign/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Parser\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-parser/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Page\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-page/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Image\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-image/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Graph\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-graph/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Font\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-font/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Filter\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-filter/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\Encrypt\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-encrypt/src'),
|
||||||
|
'Com\\Tecnick\\Pdf\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf/src'),
|
||||||
|
'Com\\Tecnick\\File\\' => array($vendorDir . '/tecnickcom/tc-lib-file/src'),
|
||||||
|
'Com\\Tecnick\\Color\\' => array($vendorDir . '/tecnickcom/tc-lib-color/src'),
|
||||||
|
'Com\\Tecnick\\Barcode\\' => array($vendorDir . '/tecnickcom/tc-lib-barcode/src'),
|
||||||
|
);
|
||||||
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_real.php @generated by Composer
|
||||||
|
|
||||||
|
class ComposerAutoloaderInit6621356a0da1644f04f6f40bd0e0b948
|
||||||
|
{
|
||||||
|
private static $loader;
|
||||||
|
|
||||||
|
public static function loadClassLoader($class)
|
||||||
|
{
|
||||||
|
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||||
|
require __DIR__ . '/ClassLoader.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Composer\Autoload\ClassLoader
|
||||||
|
*/
|
||||||
|
public static function getLoader()
|
||||||
|
{
|
||||||
|
if (null !== self::$loader) {
|
||||||
|
return self::$loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
require __DIR__ . '/platform_check.php';
|
||||||
|
|
||||||
|
spl_autoload_register(array('ComposerAutoloaderInit6621356a0da1644f04f6f40bd0e0b948', 'loadClassLoader'), true, true);
|
||||||
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||||
|
spl_autoload_unregister(array('ComposerAutoloaderInit6621356a0da1644f04f6f40bd0e0b948', 'loadClassLoader'));
|
||||||
|
|
||||||
|
require __DIR__ . '/autoload_static.php';
|
||||||
|
call_user_func(\Composer\Autoload\ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948::getInitializer($loader));
|
||||||
|
|
||||||
|
$loader->register(true);
|
||||||
|
|
||||||
|
return $loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+168
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_static.php @generated by Composer
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
|
||||||
|
{
|
||||||
|
public static $prefixLengthsPsr4 = array (
|
||||||
|
'Z' =>
|
||||||
|
array (
|
||||||
|
'ZipStream\\' => 10,
|
||||||
|
),
|
||||||
|
'P' =>
|
||||||
|
array (
|
||||||
|
'Psr\\SimpleCache\\' => 16,
|
||||||
|
'Psr\\Http\\Message\\' => 17,
|
||||||
|
'Psr\\Http\\Client\\' => 16,
|
||||||
|
'PhpOffice\\PhpSpreadsheet\\' => 25,
|
||||||
|
'PHPMailer\\PHPMailer\\' => 20,
|
||||||
|
),
|
||||||
|
'M' =>
|
||||||
|
array (
|
||||||
|
'Matrix\\' => 7,
|
||||||
|
),
|
||||||
|
'C' =>
|
||||||
|
array (
|
||||||
|
'Composer\\Pcre\\' => 14,
|
||||||
|
'Complex\\' => 8,
|
||||||
|
'Com\\Tecnick\\Unicode\\Data\\' => 25,
|
||||||
|
'Com\\Tecnick\\Unicode\\' => 20,
|
||||||
|
'Com\\Tecnick\\Pdf\\Sign\\' => 21,
|
||||||
|
'Com\\Tecnick\\Pdf\\Parser\\' => 23,
|
||||||
|
'Com\\Tecnick\\Pdf\\Page\\' => 21,
|
||||||
|
'Com\\Tecnick\\Pdf\\Image\\' => 22,
|
||||||
|
'Com\\Tecnick\\Pdf\\Graph\\' => 22,
|
||||||
|
'Com\\Tecnick\\Pdf\\Font\\' => 21,
|
||||||
|
'Com\\Tecnick\\Pdf\\Filter\\' => 23,
|
||||||
|
'Com\\Tecnick\\Pdf\\Encrypt\\' => 24,
|
||||||
|
'Com\\Tecnick\\Pdf\\' => 16,
|
||||||
|
'Com\\Tecnick\\File\\' => 17,
|
||||||
|
'Com\\Tecnick\\Color\\' => 18,
|
||||||
|
'Com\\Tecnick\\Barcode\\' => 20,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $prefixDirsPsr4 = array (
|
||||||
|
'ZipStream\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/maennchen/zipstream-php/src',
|
||||||
|
),
|
||||||
|
'Psr\\SimpleCache\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
|
||||||
|
),
|
||||||
|
'Psr\\Http\\Message\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/psr/http-factory/src',
|
||||||
|
1 => __DIR__ . '/..' . '/psr/http-message/src',
|
||||||
|
),
|
||||||
|
'Psr\\Http\\Client\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/psr/http-client/src',
|
||||||
|
),
|
||||||
|
'PhpOffice\\PhpSpreadsheet\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
|
||||||
|
),
|
||||||
|
'PHPMailer\\PHPMailer\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
|
||||||
|
),
|
||||||
|
'Matrix\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src',
|
||||||
|
),
|
||||||
|
'Composer\\Pcre\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/composer/pcre/src',
|
||||||
|
),
|
||||||
|
'Complex\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/markbaker/complex/classes/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Unicode\\Data\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-unicode-data/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Unicode\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-unicode/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Sign\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-sign/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Parser\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-parser/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Page\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-page/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Image\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-image/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Graph\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-graph/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Font\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-font/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Filter\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-filter/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\Encrypt\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-encrypt/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Pdf\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\File\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-file/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Color\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-color/src',
|
||||||
|
),
|
||||||
|
'Com\\Tecnick\\Barcode\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-barcode/src',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $classMap = array (
|
||||||
|
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||||
|
'Datamatrix' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
|
||||||
|
'PDF417' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/pdf417.php',
|
||||||
|
'QRcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/qrcode.php',
|
||||||
|
'TCPDF' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf.php',
|
||||||
|
'TCPDF2DBarcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_barcodes_2d.php',
|
||||||
|
'TCPDFBarcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_barcodes_1d.php',
|
||||||
|
'TCPDF_COLORS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_colors.php',
|
||||||
|
'TCPDF_FILTERS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_filters.php',
|
||||||
|
'TCPDF_FONTS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_fonts.php',
|
||||||
|
'TCPDF_FONT_DATA' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_font_data.php',
|
||||||
|
'TCPDF_IMAGES' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_images.php',
|
||||||
|
'TCPDF_STATIC' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_static.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
public static function getInitializer(ClassLoader $loader)
|
||||||
|
{
|
||||||
|
return \Closure::bind(function () use ($loader) {
|
||||||
|
$loader->prefixLengthsPsr4 = ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948::$prefixLengthsPsr4;
|
||||||
|
$loader->prefixDirsPsr4 = ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948::$prefixDirsPsr4;
|
||||||
|
$loader->classMap = ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948::$classMap;
|
||||||
|
|
||||||
|
}, null, ClassLoader::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1772
File diff suppressed because it is too large
Load Diff
Vendored
+248
@@ -0,0 +1,248 @@
|
|||||||
|
<?php return array(
|
||||||
|
'root' => array(
|
||||||
|
'name' => '__root__',
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev' => true,
|
||||||
|
),
|
||||||
|
'versions' => array(
|
||||||
|
'__root__' => array(
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'composer/pcre' => array(
|
||||||
|
'pretty_version' => '3.3.2',
|
||||||
|
'version' => '3.3.2.0',
|
||||||
|
'reference' => 'b2bed4734f0cc156ee1fe9c0da2550420d99a21e',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/./pcre',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'maennchen/zipstream-php' => array(
|
||||||
|
'pretty_version' => '3.2.0',
|
||||||
|
'version' => '3.2.0.0',
|
||||||
|
'reference' => '9712d8fa4cdf9240380b01eb4be55ad8dcf71416',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../maennchen/zipstream-php',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'markbaker/complex' => array(
|
||||||
|
'pretty_version' => '3.0.2',
|
||||||
|
'version' => '3.0.2.0',
|
||||||
|
'reference' => '95c56caa1cf5c766ad6d65b6344b807c1e8405b9',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../markbaker/complex',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'markbaker/matrix' => array(
|
||||||
|
'pretty_version' => '3.0.1',
|
||||||
|
'version' => '3.0.1.0',
|
||||||
|
'reference' => '728434227fe21be27ff6d86621a1b13107a2562c',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../markbaker/matrix',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'phpmailer/phpmailer' => array(
|
||||||
|
'pretty_version' => 'v7.0.0',
|
||||||
|
'version' => '7.0.0.0',
|
||||||
|
'reference' => 'c7111310c6116ba508a6a170a89eaaed2129bd42',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpmailer/phpmailer',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'phpoffice/phpspreadsheet' => array(
|
||||||
|
'pretty_version' => '5.2.0',
|
||||||
|
'version' => '5.2.0.0',
|
||||||
|
'reference' => '3b8994b3aac4b61018bc04fc8c441f4fd68c18eb',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'psr/http-client' => array(
|
||||||
|
'pretty_version' => '1.0.3',
|
||||||
|
'version' => '1.0.3.0',
|
||||||
|
'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../psr/http-client',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'psr/http-factory' => array(
|
||||||
|
'pretty_version' => '1.1.0',
|
||||||
|
'version' => '1.1.0.0',
|
||||||
|
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../psr/http-factory',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'psr/http-message' => array(
|
||||||
|
'pretty_version' => '2.0',
|
||||||
|
'version' => '2.0.0.0',
|
||||||
|
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../psr/http-message',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'psr/simple-cache' => array(
|
||||||
|
'pretty_version' => '3.0.0',
|
||||||
|
'version' => '3.0.0.0',
|
||||||
|
'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../psr/simple-cache',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-barcode' => array(
|
||||||
|
'pretty_version' => '2.13.2',
|
||||||
|
'version' => '2.13.2.0',
|
||||||
|
'reference' => 'c7f3ddbc3df10259c59ddc3016ec421267e25e5d',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-barcode',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-color' => array(
|
||||||
|
'pretty_version' => '2.13.3',
|
||||||
|
'version' => '2.13.3.0',
|
||||||
|
'reference' => '36b0cb2215c446fed65842151d494f2b3081533c',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-color',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-file' => array(
|
||||||
|
'pretty_version' => '3.7.3',
|
||||||
|
'version' => '3.7.3.0',
|
||||||
|
'reference' => 'c582b6d493c26703e95809f6d3e938c79f88885f',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-file',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf' => array(
|
||||||
|
'pretty_version' => '8.68.3',
|
||||||
|
'version' => '8.68.3.0',
|
||||||
|
'reference' => '6b056239c0d9781319a4d009e8ca33d48e94e0ea',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-encrypt' => array(
|
||||||
|
'pretty_version' => '2.9.3',
|
||||||
|
'version' => '2.9.3.0',
|
||||||
|
'reference' => '9d188937a673af8adc6da1f104c44cf434f7b198',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-encrypt',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-filter' => array(
|
||||||
|
'pretty_version' => '2.10.3',
|
||||||
|
'version' => '2.10.3.0',
|
||||||
|
'reference' => '48270d50db69852ef58a22b5b034b8a08e5c8464',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-filter',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-font' => array(
|
||||||
|
'pretty_version' => '3.13.2',
|
||||||
|
'version' => '3.13.2.0',
|
||||||
|
'reference' => '314dfbe240057e1119d4830f35827455f8a76eba',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-font',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-graph' => array(
|
||||||
|
'pretty_version' => '2.15.2',
|
||||||
|
'version' => '2.15.2.0',
|
||||||
|
'reference' => '4a3408690c65e14b9c86abe97a12c3079eb8a0cd',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-graph',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-image' => array(
|
||||||
|
'pretty_version' => '3.12.2',
|
||||||
|
'version' => '3.12.2.0',
|
||||||
|
'reference' => 'd7052a7e9216e842bd77395f1dfb443e9bfbcef8',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-image',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-page' => array(
|
||||||
|
'pretty_version' => '4.14.2',
|
||||||
|
'version' => '4.14.2.0',
|
||||||
|
'reference' => 'ac9adaddf8624b5008da335914a42a318cd7d310',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-page',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-parser' => array(
|
||||||
|
'pretty_version' => '3.14.2',
|
||||||
|
'version' => '3.14.2.0',
|
||||||
|
'reference' => 'db9c32046a3e8d57c8626e8d3ffc94caa10a0675',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-parser',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-pdf-sign' => array(
|
||||||
|
'pretty_version' => '1.1.3',
|
||||||
|
'version' => '1.1.3.0',
|
||||||
|
'reference' => '4132f1fc4b9c103b1a9ec879e2d7b7f9598918f7',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-sign',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-unicode' => array(
|
||||||
|
'pretty_version' => '3.0.2',
|
||||||
|
'version' => '3.0.2.0',
|
||||||
|
'reference' => 'e0549f1e0bee657a7a5c361d823409655a4ea2c3',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-unicode',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tc-lib-unicode-data' => array(
|
||||||
|
'pretty_version' => '3.0.2',
|
||||||
|
'version' => '3.0.2.0',
|
||||||
|
'reference' => '6b090f51613087c2ed6e576e9543c1feddf2ba51',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tc-lib-unicode-data',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'tecnickcom/tcpdf' => array(
|
||||||
|
'pretty_version' => '6.10.0',
|
||||||
|
'version' => '6.10.0.0',
|
||||||
|
'reference' => 'ca5b6de294512145db96bcbc94e61696599c391d',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../tecnickcom/tcpdf',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2021 Composer
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+189
@@ -0,0 +1,189 @@
|
|||||||
|
composer/pcre
|
||||||
|
=============
|
||||||
|
|
||||||
|
PCRE wrapping library that offers type-safe `preg_*` replacements.
|
||||||
|
|
||||||
|
This library gives you a way to ensure `preg_*` functions do not fail silently, returning
|
||||||
|
unexpected `null`s that may not be handled.
|
||||||
|
|
||||||
|
As of 3.0 this library enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage
|
||||||
|
for all matching and replaceCallback functions, [read more below](#preg_unmatched_as_null)
|
||||||
|
to understand the implications.
|
||||||
|
|
||||||
|
It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it
|
||||||
|
simplifies and reduces the possible return values from all the `preg_*` functions which
|
||||||
|
are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a
|
||||||
|
[PHPStan extension](#phpstan-extension) for parsing regular expressions and giving you even better output types.
|
||||||
|
|
||||||
|
This library is a thin wrapper around `preg_*` functions with [some limitations](#restrictions--limitations).
|
||||||
|
If you are looking for a richer API to handle regular expressions have a look at
|
||||||
|
[rawr/t-regx](https://packagist.org/packages/rawr/t-regx) instead.
|
||||||
|
|
||||||
|
[](https://github.com/composer/pcre/actions)
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install the latest version with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require composer/pcre
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
* PHP 7.4.0 is required for 3.x versions
|
||||||
|
* PHP 7.2.0 is required for 2.x versions
|
||||||
|
* PHP 5.3.2 is required for 1.x versions
|
||||||
|
|
||||||
|
|
||||||
|
Basic usage
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Instead of:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (preg_match('{fo+}', $string, $matches)) { ... }
|
||||||
|
if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... }
|
||||||
|
if (preg_match_all('{fo+}', $string, $matches)) { ... }
|
||||||
|
$newString = preg_replace('{fo+}', 'bar', $string);
|
||||||
|
$newString = preg_replace_callback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
|
||||||
|
$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
|
||||||
|
$filtered = preg_grep('{[a-z]}', $elements);
|
||||||
|
$array = preg_split('{[a-z]+}', $string);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now call these on the `Preg` class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
|
||||||
|
if (Preg::match('{fo+}', $string, $matches)) { ... }
|
||||||
|
if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... }
|
||||||
|
if (Preg::matchAll('{fo+}', $string, $matches)) { ... }
|
||||||
|
$newString = Preg::replace('{fo+}', 'bar', $string);
|
||||||
|
$newString = Preg::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string);
|
||||||
|
$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string);
|
||||||
|
$filtered = Preg::grep('{[a-z]}', $elements);
|
||||||
|
$array = Preg::split('{[a-z]+}', $string);
|
||||||
|
```
|
||||||
|
|
||||||
|
The main difference is if anything fails to match/replace/.., it will throw a `Composer\Pcre\PcreException`
|
||||||
|
instead of returning `null` (or false in some cases), so you can now use the return values safely relying on
|
||||||
|
the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split).
|
||||||
|
|
||||||
|
Additionally the `Preg` class provides match methods that return `bool` rather than `int`, for stricter type safety
|
||||||
|
when the number of pattern matches is not useful:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
|
||||||
|
if (Preg::isMatch('{fo+}', $string, $matches)) // bool
|
||||||
|
if (Preg::isMatchAll('{fo+}', $string, $matches)) // bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally the `Preg` class provides a few `*StrictGroups` method variants that ensure match groups
|
||||||
|
are always present and thus non-nullable, making it easier to write type-safe code:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
|
||||||
|
// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throw
|
||||||
|
if (Preg::matchStrictGroups('{fo+}', $string, $matches))
|
||||||
|
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This is generally safe to use as long as you do not have optional subpatterns (i.e. `(something)?`
|
||||||
|
or `(something)*` or branches with a `|` that result in some groups not being matched at all).
|
||||||
|
A subpattern that can match an empty string like `(.*)` is **not** optional, it will be present as an
|
||||||
|
empty string in the matches. A non-matching subpattern, even if optional like `(?:foo)?` will anyway not be present in
|
||||||
|
matches so it is also not a problem to use these with `*StrictGroups` methods.
|
||||||
|
|
||||||
|
If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the `Regex` class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Pcre\Regex;
|
||||||
|
|
||||||
|
// this is useful when you are just interested in knowing if something matched
|
||||||
|
// as it returns a bool instead of int(1/0) for match
|
||||||
|
$bool = Regex::isMatch('{fo+}', $string);
|
||||||
|
|
||||||
|
$result = Regex::match('{fo+}', $string);
|
||||||
|
if ($result->matched) { something($result->matches); }
|
||||||
|
|
||||||
|
$result = Regex::matchWithOffsets('{fo+}', $string);
|
||||||
|
if ($result->matched) { something($result->matches); }
|
||||||
|
|
||||||
|
$result = Regex::matchAll('{fo+}', $string);
|
||||||
|
if ($result->matched && $result->count > 3) { something($result->matches); }
|
||||||
|
|
||||||
|
$newString = Regex::replace('{fo+}', 'bar', $string)->result;
|
||||||
|
$newString = Regex::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string)->result;
|
||||||
|
$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `preg_grep` and `preg_split` are only callable via the `Preg` class as they do not have
|
||||||
|
complex return types warranting a specific result object.
|
||||||
|
|
||||||
|
See the [MatchResult](src/MatchResult.php), [MatchWithOffsetsResult](src/MatchWithOffsetsResult.php), [MatchAllResult](src/MatchAllResult.php),
|
||||||
|
[MatchAllWithOffsetsResult](src/MatchAllWithOffsetsResult.php), and [ReplaceResult](src/ReplaceResult.php) class sources for more details.
|
||||||
|
|
||||||
|
Restrictions / Limitations
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Due to type safety requirements a few restrictions are in place.
|
||||||
|
|
||||||
|
- matching using `PREG_OFFSET_CAPTURE` is made available via `matchWithOffsets` and `matchAllWithOffsets`.
|
||||||
|
You cannot pass the flag to `match`/`matchAll`.
|
||||||
|
- `Preg::split` will also reject `PREG_SPLIT_OFFSET_CAPTURE` and you should use `splitWithOffsets`
|
||||||
|
instead.
|
||||||
|
- `matchAll` rejects `PREG_SET_ORDER` as it also changes the shape of the returned matches. There
|
||||||
|
is no alternative provided as you can fairly easily code around it.
|
||||||
|
- `preg_filter` is not supported as it has a rather crazy API, most likely you should rather
|
||||||
|
use `Preg::grep` in combination with some loop and `Preg::replace`.
|
||||||
|
- `replace`, `replaceCallback` and `replaceCallbackArray` do not support an array `$subject`,
|
||||||
|
only simple strings.
|
||||||
|
- As of 2.0, the library always uses `PREG_UNMATCHED_AS_NULL` for matching, which offers [much
|
||||||
|
saner/more predictable results](#preg_unmatched_as_null). As of 3.0 the flag is also set for
|
||||||
|
`replaceCallback` and `replaceCallbackArray`.
|
||||||
|
|
||||||
|
#### PREG_UNMATCHED_AS_NULL
|
||||||
|
|
||||||
|
As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all `match*` and `isMatch*`
|
||||||
|
functions. As of 3.0 it is also done for `replaceCallback` and `replaceCallbackArray`.
|
||||||
|
|
||||||
|
This means your matches will always contain all matching groups, either as null if unmatched
|
||||||
|
or as string if it matched.
|
||||||
|
|
||||||
|
The advantages in clarity and predictability are clearer if you compare the two outputs of
|
||||||
|
running this with and without PREG_UNMATCHED_AS_NULL in $flags:
|
||||||
|
|
||||||
|
```php
|
||||||
|
preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
|
||||||
|
```
|
||||||
|
|
||||||
|
| no flag | PREG_UNMATCHED_AS_NULL |
|
||||||
|
| --- | --- |
|
||||||
|
| array (size=4) | array (size=5) |
|
||||||
|
| 0 => string 'ac' (length=2) | 0 => string 'ac' (length=2) |
|
||||||
|
| 1 => string 'a' (length=1) | 1 => string 'a' (length=1) |
|
||||||
|
| 2 => string '' (length=0) | 2 => null |
|
||||||
|
| 3 => string 'c' (length=1) | 3 => string 'c' (length=1) |
|
||||||
|
| | 4 => null |
|
||||||
|
| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for |
|
||||||
|
| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` |
|
||||||
|
|
||||||
|
PHPStan Extension
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
To use the PHPStan extension if you do not use `phpstan/extension-installer` you can include `vendor/composer/pcre/extension.neon` in your PHPStan config.
|
||||||
|
|
||||||
|
The extension provides much better type information for $matches as well as regex validation where possible.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
composer/pcre is licensed under the MIT License, see the LICENSE file for details.
|
||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "composer/pcre",
|
||||||
|
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"pcre",
|
||||||
|
"regex",
|
||||||
|
"preg",
|
||||||
|
"regular expression"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^8 || ^9",
|
||||||
|
"phpstan/phpstan": "^1.12 || ^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1 || ^2"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan": "<1.11.10"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "tests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
},
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "@php vendor/bin/phpunit",
|
||||||
|
"phpstan": "@php phpstan analyse"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# composer/pcre PHPStan extensions
|
||||||
|
#
|
||||||
|
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
|
||||||
|
# in your phpstan config
|
||||||
|
|
||||||
|
services:
|
||||||
|
-
|
||||||
|
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
|
||||||
|
tags:
|
||||||
|
- phpstan.staticMethodParameterOutTypeExtension
|
||||||
|
-
|
||||||
|
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
|
||||||
|
tags:
|
||||||
|
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
|
||||||
|
-
|
||||||
|
class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension
|
||||||
|
tags:
|
||||||
|
- phpstan.staticMethodParameterClosureTypeExtension
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
|
||||||
|
- Composer\Pcre\PHPStan\InvalidRegexPatternRule
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchAllResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => list of matched strings
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, list<string|null>>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var 0|positive-int
|
||||||
|
*/
|
||||||
|
public $count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<int|string, list<string|null>> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
$this->count = $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchAllStrictGroupsResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => list of matched strings
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, list<string>>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var 0|positive-int
|
||||||
|
*/
|
||||||
|
public $count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<list<string>> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
$this->count = $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchAllWithOffsetsResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => list of matches, every match being a pair of string matched + offset in bytes (or -1 if no match)
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, list<array{string|null, int}>>
|
||||||
|
* @phpstan-var array<int|string, list<array{string|null, int<-1, max>}>>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var 0|positive-int
|
||||||
|
*/
|
||||||
|
public $count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<int|string, list<array{string|null, int}>> $matches
|
||||||
|
* @phpstan-param array<int|string, list<array{string|null, int<-1, max>}>> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
$this->count = $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => string matched
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, string|null>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<string|null> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchStrictGroupsResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => string matched
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, string>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<string> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class MatchWithOffsetsResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* An array of match group => pair of string matched + offset in bytes (or -1 if no match)
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @var array<int|string, array{string|null, int}>
|
||||||
|
* @phpstan-var array<int|string, array{string|null, int<-1, max>}>
|
||||||
|
*/
|
||||||
|
public $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
* @param array<array{string|null, int}> $matches
|
||||||
|
* @phpstan-param array<int|string, array{string|null, int<-1, max>}> $matches
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, array $matches)
|
||||||
|
{
|
||||||
|
$this->matches = $matches;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
use Composer\Pcre\Regex;
|
||||||
|
use Composer\Pcre\PcreException;
|
||||||
|
use Nette\Utils\RegexpException;
|
||||||
|
use Nette\Utils\Strings;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Rules\Rule;
|
||||||
|
use PHPStan\Rules\RuleErrorBuilder;
|
||||||
|
use function in_array;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of PHPStan's RegularExpressionPatternRule
|
||||||
|
*
|
||||||
|
* @implements Rule<StaticCall>
|
||||||
|
*/
|
||||||
|
class InvalidRegexPatternRule implements Rule
|
||||||
|
{
|
||||||
|
public function getNodeType(): string
|
||||||
|
{
|
||||||
|
return StaticCall::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processNode(Node $node, Scope $scope): array
|
||||||
|
{
|
||||||
|
$patterns = $this->extractPatterns($node, $scope);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
$errorMessage = $this->validatePattern($pattern);
|
||||||
|
if ($errorMessage === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function extractPatterns(StaticCall $node, Scope $scope): array
|
||||||
|
{
|
||||||
|
if (!$node->class instanceof FullyQualified) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$isRegex = $node->class->toString() === Regex::class;
|
||||||
|
$isPreg = $node->class->toString() === Preg::class;
|
||||||
|
if (!$isRegex && !$isPreg) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$functionName = $node->name->name;
|
||||||
|
if (!isset($node->getArgs()[0])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$patternNode = $node->getArgs()[0]->value;
|
||||||
|
$patternType = $scope->getType($patternNode);
|
||||||
|
|
||||||
|
$patternStrings = [];
|
||||||
|
|
||||||
|
foreach ($patternType->getConstantStrings() as $constantStringType) {
|
||||||
|
if ($functionName === 'replaceCallbackArray') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$patternStrings[] = $constantStringType->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($patternType->getConstantArrays() as $constantArrayType) {
|
||||||
|
if (
|
||||||
|
in_array($functionName, [
|
||||||
|
'replace',
|
||||||
|
'replaceCallback',
|
||||||
|
], true)
|
||||||
|
) {
|
||||||
|
foreach ($constantArrayType->getValueTypes() as $arrayKeyType) {
|
||||||
|
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
|
||||||
|
$patternStrings[] = $constantString->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($functionName !== 'replaceCallbackArray') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) {
|
||||||
|
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
|
||||||
|
$patternStrings[] = $constantString->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $patternStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validatePattern(string $pattern): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$msg = null;
|
||||||
|
$prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool {
|
||||||
|
$msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($pattern === '') {
|
||||||
|
return 'Empty string is not a valid regular expression';
|
||||||
|
}
|
||||||
|
|
||||||
|
Preg::match($pattern, '');
|
||||||
|
if ($msg !== null) {
|
||||||
|
return $msg;
|
||||||
|
}
|
||||||
|
} catch (PcreException $e) {
|
||||||
|
if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) {
|
||||||
|
return $msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
restore_error_handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Type\ArrayType;
|
||||||
|
use PHPStan\Type\Constant\ConstantArrayType;
|
||||||
|
use PHPStan\Type\Constant\ConstantIntegerType;
|
||||||
|
use PHPStan\Type\IntersectionType;
|
||||||
|
use PHPStan\Type\TypeCombinator;
|
||||||
|
use PHPStan\Type\Type;
|
||||||
|
use PhpParser\Node\Arg;
|
||||||
|
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||||
|
use PHPStan\Type\TypeTraverser;
|
||||||
|
use PHPStan\Type\UnionType;
|
||||||
|
|
||||||
|
final class PregMatchFlags
|
||||||
|
{
|
||||||
|
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
|
||||||
|
{
|
||||||
|
if ($flagsArg === null) {
|
||||||
|
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagsType = $scope->getType($flagsArg->value);
|
||||||
|
|
||||||
|
$constantScalars = $flagsType->getConstantScalarValues();
|
||||||
|
if ($constantScalars === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$internalFlagsTypes = [];
|
||||||
|
foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) {
|
||||||
|
if (!is_int($constantScalarValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL);
|
||||||
|
}
|
||||||
|
return TypeCombinator::union(...$internalFlagsTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function removeNullFromMatches(Type $matchesType): Type
|
||||||
|
{
|
||||||
|
return TypeTraverser::map($matchesType, static function (Type $type, callable $traverse): Type {
|
||||||
|
if ($type instanceof UnionType || $type instanceof IntersectionType) {
|
||||||
|
return $traverse($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof ConstantArrayType) {
|
||||||
|
return new ConstantArrayType(
|
||||||
|
$type->getKeyTypes(),
|
||||||
|
array_map(static function (Type $valueType) use ($traverse): Type {
|
||||||
|
return $traverse($valueType);
|
||||||
|
}, $type->getValueTypes()),
|
||||||
|
$type->getNextAutoIndexes(),
|
||||||
|
[],
|
||||||
|
$type->isList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof ArrayType) {
|
||||||
|
return new ArrayType($type->getKeyType(), $traverse($type->getItemType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeCombinator::removeNull($type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Reflection\MethodReflection;
|
||||||
|
use PHPStan\Reflection\ParameterReflection;
|
||||||
|
use PHPStan\TrinaryLogic;
|
||||||
|
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||||
|
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
|
||||||
|
use PHPStan\Type\Type;
|
||||||
|
|
||||||
|
final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RegexArrayShapeMatcher
|
||||||
|
*/
|
||||||
|
private $regexShapeMatcher;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
RegexArrayShapeMatcher $regexShapeMatcher
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$methodReflection->getDeclaringClass()->getName() === Preg::class
|
||||||
|
&& in_array($methodReflection->getName(), [
|
||||||
|
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
|
||||||
|
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
|
||||||
|
], true)
|
||||||
|
&& $parameter->getName() === 'matches';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
|
||||||
|
{
|
||||||
|
$args = $methodCall->getArgs();
|
||||||
|
$patternArg = $args[0] ?? null;
|
||||||
|
$matchesArg = $args[2] ?? null;
|
||||||
|
$flagsArg = $args[3] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$patternArg === null || $matchesArg === null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||||
|
if ($flagsType === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
|
||||||
|
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Analyser\SpecifiedTypes;
|
||||||
|
use PHPStan\Analyser\TypeSpecifier;
|
||||||
|
use PHPStan\Analyser\TypeSpecifierAwareExtension;
|
||||||
|
use PHPStan\Analyser\TypeSpecifierContext;
|
||||||
|
use PHPStan\Reflection\MethodReflection;
|
||||||
|
use PHPStan\TrinaryLogic;
|
||||||
|
use PHPStan\Type\Constant\ConstantArrayType;
|
||||||
|
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||||
|
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
|
||||||
|
use PHPStan\Type\TypeCombinator;
|
||||||
|
use PHPStan\Type\Type;
|
||||||
|
|
||||||
|
final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TypeSpecifier
|
||||||
|
*/
|
||||||
|
private $typeSpecifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var RegexArrayShapeMatcher
|
||||||
|
*/
|
||||||
|
private $regexShapeMatcher;
|
||||||
|
|
||||||
|
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
|
||||||
|
{
|
||||||
|
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
|
||||||
|
{
|
||||||
|
$this->typeSpecifier = $typeSpecifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClass(): string
|
||||||
|
{
|
||||||
|
return Preg::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
|
||||||
|
{
|
||||||
|
return in_array($methodReflection->getName(), [
|
||||||
|
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
|
||||||
|
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
|
||||||
|
], true)
|
||||||
|
&& !$context->null();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
|
||||||
|
{
|
||||||
|
$args = $node->getArgs();
|
||||||
|
$patternArg = $args[0] ?? null;
|
||||||
|
$matchesArg = $args[2] ?? null;
|
||||||
|
$flagsArg = $args[3] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$patternArg === null || $matchesArg === null
|
||||||
|
) {
|
||||||
|
return new SpecifiedTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||||
|
if ($flagsType === null) {
|
||||||
|
return new SpecifiedTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
|
||||||
|
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
|
||||||
|
} else {
|
||||||
|
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matchedType === null) {
|
||||||
|
return new SpecifiedTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)
|
||||||
|
) {
|
||||||
|
$matchedType = PregMatchFlags::removeNullFromMatches($matchedType);
|
||||||
|
}
|
||||||
|
|
||||||
|
$overwrite = false;
|
||||||
|
if ($context->false()) {
|
||||||
|
$overwrite = true;
|
||||||
|
$context = $context->negate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @phpstan-ignore function.alreadyNarrowedType
|
||||||
|
if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) {
|
||||||
|
$typeSpecifier = $this->typeSpecifier->create(
|
||||||
|
$matchesArg->value,
|
||||||
|
$matchedType,
|
||||||
|
$context,
|
||||||
|
$scope
|
||||||
|
)->setRootExpr($node);
|
||||||
|
|
||||||
|
return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @phpstan-ignore arguments.count
|
||||||
|
return $this->typeSpecifier->create(
|
||||||
|
$matchesArg->value,
|
||||||
|
$matchedType,
|
||||||
|
$context,
|
||||||
|
// @phpstan-ignore argument.type
|
||||||
|
$overwrite,
|
||||||
|
$scope,
|
||||||
|
$node
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
use Composer\Pcre\Regex;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Reflection\MethodReflection;
|
||||||
|
use PHPStan\Reflection\Native\NativeParameterReflection;
|
||||||
|
use PHPStan\Reflection\ParameterReflection;
|
||||||
|
use PHPStan\TrinaryLogic;
|
||||||
|
use PHPStan\Type\ClosureType;
|
||||||
|
use PHPStan\Type\Constant\ConstantArrayType;
|
||||||
|
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||||
|
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
|
||||||
|
use PHPStan\Type\StringType;
|
||||||
|
use PHPStan\Type\TypeCombinator;
|
||||||
|
use PHPStan\Type\Type;
|
||||||
|
|
||||||
|
final class PregReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RegexArrayShapeMatcher
|
||||||
|
*/
|
||||||
|
private $regexShapeMatcher;
|
||||||
|
|
||||||
|
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
|
||||||
|
{
|
||||||
|
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
|
||||||
|
{
|
||||||
|
return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true)
|
||||||
|
&& in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true)
|
||||||
|
&& $parameter->getName() === 'replacement';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
|
||||||
|
{
|
||||||
|
$args = $methodCall->getArgs();
|
||||||
|
$patternArg = $args[0] ?? null;
|
||||||
|
$flagsArg = $args[5] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$patternArg === null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||||
|
|
||||||
|
$matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
|
||||||
|
if ($matchesType === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) {
|
||||||
|
$matchesType = $matchesType->getConstantArrays()[0];
|
||||||
|
$matchesType = new ConstantArrayType(
|
||||||
|
$matchesType->getKeyTypes(),
|
||||||
|
array_map(static function (Type $valueType): Type {
|
||||||
|
if (count($valueType->getConstantArrays()) === 1) {
|
||||||
|
$valueTypeArray = $valueType->getConstantArrays()[0];
|
||||||
|
return new ConstantArrayType(
|
||||||
|
$valueTypeArray->getKeyTypes(),
|
||||||
|
array_map(static function (Type $valueType): Type {
|
||||||
|
return TypeCombinator::removeNull($valueType);
|
||||||
|
}, $valueTypeArray->getValueTypes()),
|
||||||
|
$valueTypeArray->getNextAutoIndexes(),
|
||||||
|
[],
|
||||||
|
$valueTypeArray->isList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return TypeCombinator::removeNull($valueType);
|
||||||
|
}, $matchesType->getValueTypes()),
|
||||||
|
$matchesType->getNextAutoIndexes(),
|
||||||
|
[],
|
||||||
|
$matchesType->isList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClosureType(
|
||||||
|
[
|
||||||
|
new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
|
||||||
|
],
|
||||||
|
new StringType()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Composer\Pcre\PHPStan;
|
||||||
|
|
||||||
|
use Composer\Pcre\Preg;
|
||||||
|
use Composer\Pcre\Regex;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
use PHPStan\Analyser\Scope;
|
||||||
|
use PHPStan\Analyser\SpecifiedTypes;
|
||||||
|
use PHPStan\Rules\Rule;
|
||||||
|
use PHPStan\Rules\RuleErrorBuilder;
|
||||||
|
use PHPStan\TrinaryLogic;
|
||||||
|
use PHPStan\Type\ObjectType;
|
||||||
|
use PHPStan\Type\Type;
|
||||||
|
use PHPStan\Type\TypeCombinator;
|
||||||
|
use PHPStan\Type\Php\RegexArrayShapeMatcher;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Rule<StaticCall>
|
||||||
|
*/
|
||||||
|
final class UnsafeStrictGroupsCallRule implements Rule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RegexArrayShapeMatcher
|
||||||
|
*/
|
||||||
|
private $regexShapeMatcher;
|
||||||
|
|
||||||
|
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
|
||||||
|
{
|
||||||
|
$this->regexShapeMatcher = $regexShapeMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNodeType(): string
|
||||||
|
{
|
||||||
|
return StaticCall::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processNode(Node $node, Scope $scope): array
|
||||||
|
{
|
||||||
|
if (!$node->class instanceof FullyQualified) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$isRegex = $node->class->toString() === Regex::class;
|
||||||
|
$isPreg = $node->class->toString() === Preg::class;
|
||||||
|
if (!$isRegex && !$isPreg) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = $node->getArgs();
|
||||||
|
if (!isset($args[0])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$patternArg = $args[0] ?? null;
|
||||||
|
if ($isPreg) {
|
||||||
|
if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$flagsArg = $args[3] ?? null;
|
||||||
|
} else {
|
||||||
|
$flagsArg = $args[2] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($patternArg === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
|
||||||
|
if ($flagsType === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
|
||||||
|
if ($matchedType === null) {
|
||||||
|
return [
|
||||||
|
RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name))
|
||||||
|
->identifier('composerPcre.maybeUnsafeStrictGroups')
|
||||||
|
->build(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($matchedType->getConstantArrays()) === 1) {
|
||||||
|
$matchedType = $matchedType->getConstantArrays()[0];
|
||||||
|
$nullableGroups = [];
|
||||||
|
foreach ($matchedType->getValueTypes() as $index => $type) {
|
||||||
|
if (TypeCombinator::containsNull($type)) {
|
||||||
|
$nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($nullableGroups) > 0) {
|
||||||
|
return [
|
||||||
|
RuleErrorBuilder::message(sprintf(
|
||||||
|
'The %s call is unsafe as match group%s "%s" %s optional and may be null.',
|
||||||
|
$node->name->name,
|
||||||
|
\count($nullableGroups) > 1 ? 's' : '',
|
||||||
|
implode('", "', $nullableGroups),
|
||||||
|
\count($nullableGroups) > 1 ? 'are' : 'is'
|
||||||
|
))->identifier('composerPcre.unsafeStrictGroups')->build(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
class PcreException extends \RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $function
|
||||||
|
* @param string|string[] $pattern
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromFunction($function, $pattern)
|
||||||
|
{
|
||||||
|
$code = preg_last_error();
|
||||||
|
|
||||||
|
if (is_array($pattern)) {
|
||||||
|
$pattern = implode(', ', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PcreException($function.'(): failed executing "'.$pattern.'": '.self::pcreLastErrorMessage($code), $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $code
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function pcreLastErrorMessage($code)
|
||||||
|
{
|
||||||
|
if (function_exists('preg_last_error_msg')) {
|
||||||
|
return preg_last_error_msg();
|
||||||
|
}
|
||||||
|
|
||||||
|
$constants = get_defined_constants(true);
|
||||||
|
if (!isset($constants['pcre']) || !is_array($constants['pcre'])) {
|
||||||
|
return 'UNDEFINED_ERROR';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($constants['pcre'] as $const => $val) {
|
||||||
|
if ($val === $code && substr($const, -6) === '_ERROR') {
|
||||||
|
return $const;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'UNDEFINED_ERROR';
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+430
@@ -0,0 +1,430 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
class Preg
|
||||||
|
{
|
||||||
|
/** @internal */
|
||||||
|
public const ARRAY_MSG = '$subject as an array is not supported. You can use \'foreach\' instead.';
|
||||||
|
/** @internal */
|
||||||
|
public const INVALID_TYPE_MSG = '$subject must be a string, %s given.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @return 0|1
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, string|null> $matches
|
||||||
|
*/
|
||||||
|
public static function match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
self::checkOffsetCapture($flags, 'matchWithOffsets');
|
||||||
|
|
||||||
|
$result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset);
|
||||||
|
if ($result === false) {
|
||||||
|
throw PcreException::fromFunction('preg_match', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `match()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @return 0|1
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, string> $matches
|
||||||
|
*/
|
||||||
|
public static function matchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
$result = self::match($pattern, $subject, $matchesInternal, $flags, $offset);
|
||||||
|
$matches = self::enforceNonNullMatches($pattern, $matchesInternal, 'match');
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs preg_match with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_UNMATCHED_AS_NULL and PREG_OFFSET_CAPTURE are always set, no other flags are supported
|
||||||
|
* @return 0|1
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, array{string|null, int<-1, max>}> $matches
|
||||||
|
*/
|
||||||
|
public static function matchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
$result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset);
|
||||||
|
if ($result === false) {
|
||||||
|
throw PcreException::fromFunction('preg_match', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @return 0|positive-int
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<string|null>> $matches
|
||||||
|
*/
|
||||||
|
public static function matchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
self::checkOffsetCapture($flags, 'matchAllWithOffsets');
|
||||||
|
self::checkSetOrder($flags);
|
||||||
|
|
||||||
|
$result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset);
|
||||||
|
if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false
|
||||||
|
throw PcreException::fromFunction('preg_match_all', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `match()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @return 0|positive-int
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<string>> $matches
|
||||||
|
*/
|
||||||
|
public static function matchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
$result = self::matchAll($pattern, $subject, $matchesInternal, $flags, $offset);
|
||||||
|
$matches = self::enforceNonNullMatchAll($pattern, $matchesInternal, 'matchAll');
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs preg_match_all with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported
|
||||||
|
* @return 0|positive-int
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<array{string|null, int<-1, max>}>> $matches
|
||||||
|
*/
|
||||||
|
public static function matchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int
|
||||||
|
{
|
||||||
|
self::checkSetOrder($flags);
|
||||||
|
|
||||||
|
$result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset);
|
||||||
|
if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false
|
||||||
|
throw PcreException::fromFunction('preg_match_all', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|string[] $pattern
|
||||||
|
* @param string|string[] $replacement
|
||||||
|
* @param string $subject
|
||||||
|
* @param int $count Set by method
|
||||||
|
*
|
||||||
|
* @param-out int<0, max> $count
|
||||||
|
*/
|
||||||
|
public static function replace($pattern, $replacement, $subject, int $limit = -1, ?int &$count = null): string
|
||||||
|
{
|
||||||
|
if (!is_scalar($subject)) {
|
||||||
|
if (is_array($subject)) {
|
||||||
|
throw new \InvalidArgumentException(static::ARRAY_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = preg_replace($pattern, $replacement, $subject, $limit, $count);
|
||||||
|
if ($result === null) {
|
||||||
|
throw PcreException::fromFunction('preg_replace', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|string[] $pattern
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array<int|string, array{string|null, int<-1, max>}>): string) : callable(array<int|string, string|null>): string) $replacement
|
||||||
|
* @param string $subject
|
||||||
|
* @param int $count Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*
|
||||||
|
* @param-out int<0, max> $count
|
||||||
|
*/
|
||||||
|
public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string
|
||||||
|
{
|
||||||
|
if (!is_scalar($subject)) {
|
||||||
|
if (is_array($subject)) {
|
||||||
|
throw new \InvalidArgumentException(static::ARRAY_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL);
|
||||||
|
if ($result === null) {
|
||||||
|
throw PcreException::fromFunction('preg_replace_callback', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `replaceCallback()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param string $pattern
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array<int|string, array{string, int<0, max>}>): string) : callable(array<int|string, string>): string) $replacement
|
||||||
|
* @param string $subject
|
||||||
|
* @param int $count Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*
|
||||||
|
* @param-out int<0, max> $count
|
||||||
|
*/
|
||||||
|
public static function replaceCallbackStrictGroups(string $pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string
|
||||||
|
{
|
||||||
|
return self::replaceCallback($pattern, function (array $matches) use ($pattern, $replacement) {
|
||||||
|
return $replacement(self::enforceNonNullMatches($pattern, $matches, 'replaceCallback'));
|
||||||
|
}, $subject, $limit, $count, $flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (array<string, callable(array<int|string, array{string|null, int<-1, max>}>): string>) : array<string, callable(array<int|string, string|null>): string>) $pattern
|
||||||
|
* @param string $subject
|
||||||
|
* @param int $count Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*
|
||||||
|
* @param-out int<0, max> $count
|
||||||
|
*/
|
||||||
|
public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string
|
||||||
|
{
|
||||||
|
if (!is_scalar($subject)) {
|
||||||
|
if (is_array($subject)) {
|
||||||
|
throw new \InvalidArgumentException(static::ARRAY_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = preg_replace_callback_array($pattern, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL);
|
||||||
|
if ($result === null) {
|
||||||
|
$pattern = array_keys($pattern);
|
||||||
|
throw PcreException::fromFunction('preg_replace_callback_array', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int-mask<PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_OFFSET_CAPTURE> $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array
|
||||||
|
{
|
||||||
|
if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) {
|
||||||
|
throw new \InvalidArgumentException('PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = preg_split($pattern, $subject, $limit, $flags);
|
||||||
|
if ($result === false) {
|
||||||
|
throw PcreException::fromFunction('preg_split', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int-mask<PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_OFFSET_CAPTURE> $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_OFFSET_CAPTURE is always set
|
||||||
|
* @return list<array{string, int}>
|
||||||
|
* @phpstan-return list<array{string, int<0, max>}>
|
||||||
|
*/
|
||||||
|
public static function splitWithOffsets(string $pattern, string $subject, int $limit = -1, int $flags = 0): array
|
||||||
|
{
|
||||||
|
$result = preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE);
|
||||||
|
if ($result === false) {
|
||||||
|
throw PcreException::fromFunction('preg_split', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of string|\Stringable
|
||||||
|
* @param string $pattern
|
||||||
|
* @param array<T> $array
|
||||||
|
* @param int-mask<PREG_GREP_INVERT> $flags PREG_GREP_INVERT
|
||||||
|
* @return array<T>
|
||||||
|
*/
|
||||||
|
public static function grep(string $pattern, array $array, int $flags = 0): array
|
||||||
|
{
|
||||||
|
$result = preg_grep($pattern, $array, $flags);
|
||||||
|
if ($result === false) {
|
||||||
|
throw PcreException::fromFunction('preg_grep', $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of match() which returns a bool instead of int
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, string|null> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatch(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) static::match($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `isMatch()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, string> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) self::matchStrictGroups($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of matchAll() which returns a bool instead of int
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<string|null>> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) static::matchAll($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `isMatchAll()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<string>> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) self::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of matchWithOffsets() which returns a bool instead of int
|
||||||
|
*
|
||||||
|
* Runs preg_match with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, array{string|null, int<-1, max>}> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) static::matchWithOffsets($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of matchAllWithOffsets() which returns a bool instead of int
|
||||||
|
*
|
||||||
|
* Runs preg_match_all with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param array<mixed> $matches Set by method
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*
|
||||||
|
* @param-out array<int|string, list<array{string|null, int<-1, max>}>> $matches
|
||||||
|
*/
|
||||||
|
public static function isMatchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) static::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function checkOffsetCapture(int $flags, string $useFunctionName): void
|
||||||
|
{
|
||||||
|
if (($flags & PREG_OFFSET_CAPTURE) !== 0) {
|
||||||
|
throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use ' . $useFunctionName . '() instead');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function checkSetOrder(int $flags): void
|
||||||
|
{
|
||||||
|
if (($flags & PREG_SET_ORDER) !== 0) {
|
||||||
|
throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the type of $matches');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int|string, string|null|array{string|null, int}> $matches
|
||||||
|
* @return array<int|string, string>
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*/
|
||||||
|
private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod)
|
||||||
|
{
|
||||||
|
foreach ($matches as $group => $match) {
|
||||||
|
if (is_string($match) || (is_array($match) && is_string($match[0]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<string> */
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int|string, list<string|null>> $matches
|
||||||
|
* @return array<int|string, list<string>>
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*/
|
||||||
|
private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod)
|
||||||
|
{
|
||||||
|
foreach ($matches as $group => $groupMatches) {
|
||||||
|
foreach ($groupMatches as $match) {
|
||||||
|
if (null === $match) {
|
||||||
|
throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<int|string, list<string>> */
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+176
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
class Regex
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
*/
|
||||||
|
public static function isMatch(string $pattern, string $subject, int $offset = 0): bool
|
||||||
|
{
|
||||||
|
return (bool) Preg::match($pattern, $subject, $matches, 0, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*/
|
||||||
|
public static function match(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchResult
|
||||||
|
{
|
||||||
|
self::checkOffsetCapture($flags, 'matchWithOffsets');
|
||||||
|
|
||||||
|
$count = Preg::match($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchResult($count, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `match()` which returns non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*/
|
||||||
|
public static function matchStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchStrictGroupsResult
|
||||||
|
{
|
||||||
|
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
|
||||||
|
$count = Preg::matchStrictGroups($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchStrictGroupsResult($count, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs preg_match with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported
|
||||||
|
*/
|
||||||
|
public static function matchWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchWithOffsetsResult
|
||||||
|
{
|
||||||
|
$count = Preg::matchWithOffsets($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchWithOffsetsResult($count, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
*/
|
||||||
|
public static function matchAll(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllResult
|
||||||
|
{
|
||||||
|
self::checkOffsetCapture($flags, 'matchAllWithOffsets');
|
||||||
|
self::checkSetOrder($flags);
|
||||||
|
|
||||||
|
$count = Preg::matchAll($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchAllResult($count, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `matchAll()` which returns non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL> $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported
|
||||||
|
* @throws UnexpectedNullMatchException
|
||||||
|
*/
|
||||||
|
public static function matchAllStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllStrictGroupsResult
|
||||||
|
{
|
||||||
|
self::checkOffsetCapture($flags, 'matchAllWithOffsets');
|
||||||
|
self::checkSetOrder($flags);
|
||||||
|
|
||||||
|
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
|
||||||
|
$count = Preg::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchAllStrictGroupsResult($count, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs preg_match_all with PREG_OFFSET_CAPTURE
|
||||||
|
*
|
||||||
|
* @param non-empty-string $pattern
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported
|
||||||
|
*/
|
||||||
|
public static function matchAllWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllWithOffsetsResult
|
||||||
|
{
|
||||||
|
self::checkSetOrder($flags);
|
||||||
|
|
||||||
|
$count = Preg::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset);
|
||||||
|
|
||||||
|
return new MatchAllWithOffsetsResult($count, $matches);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param string|string[] $pattern
|
||||||
|
* @param string|string[] $replacement
|
||||||
|
* @param string $subject
|
||||||
|
*/
|
||||||
|
public static function replace($pattern, $replacement, $subject, int $limit = -1): ReplaceResult
|
||||||
|
{
|
||||||
|
$result = Preg::replace($pattern, $replacement, $subject, $limit, $count);
|
||||||
|
|
||||||
|
return new ReplaceResult($count, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|string[] $pattern
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array<int|string, array{string|null, int<-1, max>}>): string) : callable(array<int|string, string|null>): string) $replacement
|
||||||
|
* @param string $subject
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*/
|
||||||
|
public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult
|
||||||
|
{
|
||||||
|
$result = Preg::replaceCallback($pattern, $replacement, $subject, $limit, $count, $flags);
|
||||||
|
|
||||||
|
return new ReplaceResult($count, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of `replaceCallback()` which outputs non-null matches (or throws)
|
||||||
|
*
|
||||||
|
* @param string $pattern
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array<int|string, array{string, int<0, max>}>): string) : callable(array<int|string, string>): string) $replacement
|
||||||
|
* @param string $subject
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*/
|
||||||
|
public static function replaceCallbackStrictGroups($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult
|
||||||
|
{
|
||||||
|
$result = Preg::replaceCallbackStrictGroups($pattern, $replacement, $subject, $limit, $count, $flags);
|
||||||
|
|
||||||
|
return new ReplaceResult($count, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ($flags is PREG_OFFSET_CAPTURE ? (array<string, callable(array<int|string, array{string|null, int<-1, max>}>): string>) : array<string, callable(array<int|string, string|null>): string>) $pattern
|
||||||
|
* @param string $subject
|
||||||
|
* @param int-mask<PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE> $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set
|
||||||
|
*/
|
||||||
|
public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, int $flags = 0): ReplaceResult
|
||||||
|
{
|
||||||
|
$result = Preg::replaceCallbackArray($pattern, $subject, $limit, $count, $flags);
|
||||||
|
|
||||||
|
return new ReplaceResult($count, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function checkOffsetCapture(int $flags, string $useFunctionName): void
|
||||||
|
{
|
||||||
|
if (($flags & PREG_OFFSET_CAPTURE) !== 0) {
|
||||||
|
throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the return type, use '.$useFunctionName.'() instead');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function checkSetOrder(int $flags): void
|
||||||
|
{
|
||||||
|
if (($flags & PREG_SET_ORDER) !== 0) {
|
||||||
|
throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the return type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
final class ReplaceResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var 0|positive-int
|
||||||
|
*/
|
||||||
|
public $count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $matched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param 0|positive-int $count
|
||||||
|
*/
|
||||||
|
public function __construct(int $count, string $result)
|
||||||
|
{
|
||||||
|
$this->count = $count;
|
||||||
|
$this->matched = (bool) $count;
|
||||||
|
$this->result = $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/pcre.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Pcre;
|
||||||
|
|
||||||
|
class UnexpectedNullMatchException extends PcreException
|
||||||
|
{
|
||||||
|
public static function fromFunction($function, $pattern)
|
||||||
|
{
|
||||||
|
throw new \LogicException('fromFunction should not be called on '.self::class.', use '.PcreException::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// platform_check.php @generated by Composer
|
||||||
|
|
||||||
|
$issues = array();
|
||||||
|
|
||||||
|
if (!(PHP_VERSION_ID >= 80300)) {
|
||||||
|
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.3.0". You are running ' . PHP_VERSION . '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_INT_SIZE !== 8) {
|
||||||
|
$issues[] = 'Your Composer dependencies require a 64-bit build of PHP.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issues) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||||
|
);
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.{yml,md,xml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{rst,php}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[composer.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[composer.lock]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phive xmlns="https://phar.io/phive">
|
||||||
|
<phar name="phpdocumentor" version="^3.3.1" installed="3.8.0" location="./tools/phpdocumentor" copy="false"/>
|
||||||
|
</phive>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP-CS-Fixer config for ZipStream-PHP
|
||||||
|
* @author Nicolas CARPi <nico-git@deltablot.email>
|
||||||
|
* @copyright 2022 Nicolas CARPi
|
||||||
|
* @see https://github.com/maennchen/ZipStream-PHP
|
||||||
|
* @license MIT
|
||||||
|
* @package maennchen/ZipStream-PHP
|
||||||
|
*/
|
||||||
|
|
||||||
|
use PhpCsFixer\Config;
|
||||||
|
use PhpCsFixer\Finder;
|
||||||
|
use PhpCsFixer\Runner;
|
||||||
|
|
||||||
|
$finder = Finder::create()
|
||||||
|
->exclude('.github')
|
||||||
|
->exclude('.phpdoc')
|
||||||
|
->exclude('docs')
|
||||||
|
->exclude('tools')
|
||||||
|
->exclude('vendor')
|
||||||
|
->in(__DIR__);
|
||||||
|
|
||||||
|
$config = new Config();
|
||||||
|
return $config->setRules([
|
||||||
|
'@PER' => true,
|
||||||
|
'@PER:risky' => true,
|
||||||
|
'@PHP83Migration' => true,
|
||||||
|
// Enable once PHP 8.4 is the minimum version
|
||||||
|
// '@PHP84Migration' => true,
|
||||||
|
'@PHPUnit84Migration:risky' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'class_attributes_separation' => true,
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'dir_constant' => true,
|
||||||
|
'is_null' => true,
|
||||||
|
'no_homoglyph_names' => true,
|
||||||
|
'no_null_property_initialization' => true,
|
||||||
|
'no_php4_constructor' => true,
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'non_printable_character' => true,
|
||||||
|
'ordered_imports' => true,
|
||||||
|
'ordered_class_elements' => true,
|
||||||
|
'php_unit_construct' => true,
|
||||||
|
'pow_to_exponentiation' => true,
|
||||||
|
'psr_autoloading' => true,
|
||||||
|
'random_api_migration' => true,
|
||||||
|
'return_assignment' => true,
|
||||||
|
'self_accessor' => true,
|
||||||
|
'semicolon_after_instruction' => true,
|
||||||
|
'short_scalar_cast' => true,
|
||||||
|
'simplified_null_return' => true,
|
||||||
|
'single_class_element_per_statement' => true,
|
||||||
|
'single_line_comment_style' => true,
|
||||||
|
'single_quote' => true,
|
||||||
|
'space_after_semicolon' => true,
|
||||||
|
'standardize_not_equals' => true,
|
||||||
|
'strict_param' => true,
|
||||||
|
'ternary_operator_spaces' => true,
|
||||||
|
'trailing_comma_in_multiline' => true,
|
||||||
|
'trim_array_spaces' => true,
|
||||||
|
'unary_operator_spaces' => true,
|
||||||
|
'global_namespace_import' => [
|
||||||
|
'import_classes' => true,
|
||||||
|
'import_functions' => true,
|
||||||
|
'import_constants' => true,
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->setFinder($finder)
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setParallelConfig(Runner\Parallel\ParallelConfigFactory::detect());
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'layout.html.twig' %}
|
||||||
|
|
||||||
|
{% set topMenu = {
|
||||||
|
"menu": [
|
||||||
|
{ "name": "Guides", "url": "https://maennchen.dev/ZipStream-PHP/guide/index.html"},
|
||||||
|
{ "name": "API", "url": "https://maennchen.dev/ZipStream-PHP/classes/ZipStream-ZipStream.html"},
|
||||||
|
{ "name": "Issues", "url": "https://github.com/maennchen/ZipStream-PHP/issues"},
|
||||||
|
],
|
||||||
|
"social": [
|
||||||
|
{ "iconClass": "fab fa-github", "url": "https://github.com/maennchen/ZipStream-PHP"},
|
||||||
|
{ "iconClass": "fas fa-envelope-open-text", "url": "https://github.com/maennchen/ZipStream-PHP/discussions"},
|
||||||
|
{ "iconClass": "fas fa-money-bill", "url": "https://github.com/sponsors/maennchen"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
%}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
php 8.4.10
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org>
|
||||||
|
Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch>
|
||||||
|
Copyright (C) 2014 Jesse G. Donat <donatj@gmail.com>
|
||||||
|
Copyright (C) 2018 Nicolas CARPi <nicolas.carpi@curie.fr>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
# ZipStream-PHP
|
||||||
|
|
||||||
|
[](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml)
|
||||||
|
[](https://coveralls.io/github/maennchen/ZipStream-PHP?branch=main)
|
||||||
|
[](https://packagist.org/packages/maennchen/zipstream-php)
|
||||||
|
[](https://packagist.org/packages/maennchen/zipstream-php)
|
||||||
|
[](https://www.bestpractices.dev/projects/9524)
|
||||||
|
[](https://scorecard.dev/viewer/?uri=github.com/maennchen/ZipStream-PHP)
|
||||||
|
|
||||||
|
## Unstable Branch
|
||||||
|
|
||||||
|
The `main` branch is not stable. Please see the
|
||||||
|
[releases](https://github.com/maennchen/ZipStream-PHP/releases) for a stable
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require maennchen/zipstream-php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
For detailed instructions, please check the
|
||||||
|
[Documentation](https://maennchen.github.io/ZipStream-PHP/).
|
||||||
|
|
||||||
|
```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
|
||||||
|
sendHttpHeaders: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create a file named 'hello.txt'
|
||||||
|
$zip->addFile(
|
||||||
|
fileName: 'hello.txt',
|
||||||
|
data: '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',
|
||||||
|
);
|
||||||
|
|
||||||
|
// finish the zip stream
|
||||||
|
$zip->finish();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Callback Output
|
||||||
|
|
||||||
|
You can stream ZIP data to a custom callback function instead of directly to the browser:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ZipStream\ZipStream;
|
||||||
|
use ZipStream\Stream\CallbackStreamWrapper;
|
||||||
|
|
||||||
|
// Stream to a callback function with proper file handling
|
||||||
|
$outputFile = fopen('output.zip', 'wb');
|
||||||
|
$backupFile = fopen('backup.zip', 'wb');
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: CallbackStreamWrapper::open(function (string $data) use ($outputFile, $backupFile) {
|
||||||
|
// Handle ZIP data as it's generated
|
||||||
|
fwrite($outputFile, $data);
|
||||||
|
|
||||||
|
// Send to multiple destinations efficiently
|
||||||
|
echo $data; // Browser
|
||||||
|
fwrite($backupFile, $data); // Backup file
|
||||||
|
}),
|
||||||
|
sendHttpHeaders: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('hello.txt', 'Hello World!');
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
fclose($outputFile);
|
||||||
|
fclose($backupFile);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
**💬 Questions? Please Read This First!**
|
||||||
|
|
||||||
|
If you have a question about using this library, please *do not email the
|
||||||
|
authors directly*. Instead, head over to the
|
||||||
|
[GitHub Discussions](https://github.com/maennchen/ZipStream-PHP/discussions)
|
||||||
|
page — your question might already be answered there! Using Discussions helps
|
||||||
|
build a shared knowledge base, so others can also benefit from the answers. If
|
||||||
|
you need dedicated 1:1 support, check out the options available on
|
||||||
|
[@maennchen's sponsorship page](https://github.com/sponsors/maennchen?frequency=one-time&sponsor=maennchen).
|
||||||
|
|
||||||
|
## Upgrade to version 3.1.2
|
||||||
|
|
||||||
|
- Minimum PHP Version: `8.2`
|
||||||
|
|
||||||
|
## 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 classes 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` constructor.
|
||||||
|
- 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` constructor 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
|
||||||
|
|
||||||
|
## Upgrade to version 1.0.0
|
||||||
|
|
||||||
|
https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-100
|
||||||
|
|
||||||
|
## 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/
|
||||||
|
- Jonatan Männchen <jonatan@maennchen.ch> - https://maennchen.dev
|
||||||
|
- Jesse G. Donat <donatj@gmail.com> - https://donatstudios.com
|
||||||
|
- Nicolas CARPi <nico-git@deltablot.email> - https://www.deltablot.com
|
||||||
|
- Nik Barham <nik@brokencube.co.uk> - https://www.brokencube.co.uk
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"name": "maennchen/zipstream-php",
|
||||||
|
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||||
|
"keywords": ["zip", "stream"],
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [{
|
||||||
|
"name": "Paul Duncan",
|
||||||
|
"email": "pabs@pablotron.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonatan Männchen",
|
||||||
|
"email": "jonatan@maennchen.ch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jesse Donat",
|
||||||
|
"email": "donatj@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "András Kolesár",
|
||||||
|
"email": "kolesar@kolesar.hu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php-64bit": "^8.3",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-zlib": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"mikey179/vfsstream": "^1.6",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.16",
|
||||||
|
"vimeo/psalm": "^6.0",
|
||||||
|
"brianium/paratest": "^7.7"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"psr/http-message": "^2.0",
|
||||||
|
"guzzlehttp/psr7": "^2.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"format": "php-cs-fixer fix",
|
||||||
|
"test": [
|
||||||
|
"@test:unit",
|
||||||
|
"@test:formatted",
|
||||||
|
"@test:lint"
|
||||||
|
],
|
||||||
|
"test:unit:setup-cov": "@putenv XDEBUG_MODE=coverage",
|
||||||
|
"test:unit": "paratest --functional",
|
||||||
|
"test:unit:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov"],
|
||||||
|
"test:unit:slow": "@test:unit --group slow",
|
||||||
|
"test:unit:slow:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov --group slow"],
|
||||||
|
"test:unit:fast": "@test:unit --exclude-group slow",
|
||||||
|
"test:unit:fast:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov --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",
|
||||||
|
"coverage:report": "php-coveralls --coverage_clover=coverage.clover.xml --json_path=coveralls-upload.json --insecure",
|
||||||
|
"install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656 --trust-gpg-keys 0x8AC0BAA79732DD42 --trust-gpg-keys 0x6DA3ACC4991FFAE5",
|
||||||
|
"docs:generate": "tools/phpdocumentor --sourcecode"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ZipStream\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": { "ZipStream\\Test\\": "test/" }
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"exclude": [
|
||||||
|
"/composer.lock",
|
||||||
|
"/docs",
|
||||||
|
"/.gitattributes",
|
||||||
|
"/.github",
|
||||||
|
"/.gitignore",
|
||||||
|
"/guides",
|
||||||
|
"/.phive",
|
||||||
|
"/.php-cs-fixer.cache",
|
||||||
|
"/.php-cs-fixer.dist.php",
|
||||||
|
"/.phpdoc",
|
||||||
|
"/phpdoc.dist.xml",
|
||||||
|
"/.phpunit.result.cache",
|
||||||
|
"/phpunit.xml.dist",
|
||||||
|
"/psalm.xml",
|
||||||
|
"/test",
|
||||||
|
"/tools",
|
||||||
|
"/.tool-versions",
|
||||||
|
"/vendor"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
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 necessary.
|
||||||
|
|
||||||
|
``SIMULATION_STRICT`` is therefore useful to make sure that the size can be
|
||||||
|
calculated efficiently.
|
||||||
|
|
||||||
|
.. 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Normally add files
|
||||||
|
$zip->addFile('sample.txt', 'Sample String Data');
|
||||||
|
|
||||||
|
// Use addFileFromCallback and exactSize if you want to defer opening of
|
||||||
|
// the file resource
|
||||||
|
$zip->addFileFromCallback(
|
||||||
|
'sample.txt',
|
||||||
|
exactSize: 18,
|
||||||
|
callback: function () {
|
||||||
|
return fopen('...');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
Usage with FlySystem
|
||||||
|
===============
|
||||||
|
|
||||||
|
For saving or uploading the generated zip, you can use the
|
||||||
|
`Flysystem <https://flysystem.thephpleague.com>`_ package, and its many
|
||||||
|
adapters.
|
||||||
|
|
||||||
|
For that you will need to provide another stream than the ``php://output``
|
||||||
|
default one, and pass it to Flysystem ``putStream`` method.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
// Open Stream only once for read and write since it's a memory stream and
|
||||||
|
// the content is lost when closing the stream / opening another one
|
||||||
|
$tempStream = fopen('php://memory', 'w+');
|
||||||
|
|
||||||
|
// Create Zip Archive
|
||||||
|
$zipStream = new ZipStream(
|
||||||
|
outputStream: $tempStream,
|
||||||
|
outputName: 'test.zip',
|
||||||
|
);
|
||||||
|
$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');
|
||||||
|
$filesystem = new Filesystem($adapter);
|
||||||
|
|
||||||
|
$filesystem->writeStream('test.zip', $tempStream)
|
||||||
|
|
||||||
|
// Close Stream
|
||||||
|
fclose($tempStream);
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
Usage with nginx
|
||||||
|
=============
|
||||||
|
|
||||||
|
If you are using nginx as a webserver, it will try to buffer the response.
|
||||||
|
So you'll want to disable this with a custom header:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
header('X-Accel-Buffering: no');
|
||||||
|
# or with the Response class from Symfony
|
||||||
|
$response->headers->set('X-Accel-Buffering', 'no');
|
||||||
|
|
||||||
|
Alternatively, you can tweak the
|
||||||
|
`fastcgi cache parameters <https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_buffers>`_
|
||||||
|
within nginx config.
|
||||||
|
|
||||||
|
See `original issue <https://github.com/maennchen/ZipStream-PHP/issues/77>`_.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
Available options
|
||||||
|
===============
|
||||||
|
|
||||||
|
Here is the full list of options available to you. You can also have a look at
|
||||||
|
``src/ZipStream.php`` file.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use ZipStream\ZipStream;
|
||||||
|
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
// Define output stream
|
||||||
|
// (argument is either a resource or implementing
|
||||||
|
// `Psr\Http\Message\StreamInterface`)
|
||||||
|
//
|
||||||
|
// Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies
|
||||||
|
// required when using `Psr\Http\Message\StreamInterface`.
|
||||||
|
//
|
||||||
|
// Can also use CallbackStreamWrapper for custom output handling:
|
||||||
|
// outputStream: CallbackStreamWrapper::open(function($data) { /* handle data */ }),
|
||||||
|
outputStream: $filePointer,
|
||||||
|
|
||||||
|
// Set the deflate level (default is 6; use -1 to disable it)
|
||||||
|
defaultDeflateLevel: 6,
|
||||||
|
|
||||||
|
// Add a comment to the zip file
|
||||||
|
comment: 'This is a comment.',
|
||||||
|
|
||||||
|
// Send http headers (default is true)
|
||||||
|
sendHttpHeaders: 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.
|
||||||
|
contentDisposition: 'attachment',
|
||||||
|
|
||||||
|
// Output Name for HTTP Content-Disposition
|
||||||
|
// Defaults to no name
|
||||||
|
outputName: "example.zip",
|
||||||
|
|
||||||
|
// HTTP Content-Type.
|
||||||
|
// Defaults to 'application/x-zip'.
|
||||||
|
// Note that this does nothing if you are not sending HTTP headers.
|
||||||
|
contentType: 'application/x-zip',
|
||||||
|
|
||||||
|
// Set the function called for setting headers.
|
||||||
|
// Default is the `header()` of PHP
|
||||||
|
httpHeaderCallback: header(...),
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// Enable zip64 extension, allowing very large archives
|
||||||
|
// (> 4Gb or file count > 64k)
|
||||||
|
// Default is true
|
||||||
|
enableZip64: true,
|
||||||
|
|
||||||
|
// Flush output buffer after every write
|
||||||
|
// Default is false
|
||||||
|
flushOutput: true,
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Usage with PSR 7 Streams
|
||||||
|
===============
|
||||||
|
|
||||||
|
PSR-7 streams are `standardized streams <https://www.php-fig.org/psr/psr-7/>`_.
|
||||||
|
|
||||||
|
ZipStream-PHP supports working with these streams with the function
|
||||||
|
``addFileFromPsr7Stream``.
|
||||||
|
|
||||||
|
For all parameters of the function see the API documentation.
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
Stream Output
|
||||||
|
===============
|
||||||
|
|
||||||
|
Stream to S3 Bucket
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Aws\S3\S3Client;
|
||||||
|
use Aws\Credentials\CredentialProvider;
|
||||||
|
use ZipStream\ZipStream;
|
||||||
|
|
||||||
|
$bucket = 'your bucket name';
|
||||||
|
$client = new S3Client([
|
||||||
|
'region' => 'your region',
|
||||||
|
'version' => 'latest',
|
||||||
|
'bucketName' => $bucket,
|
||||||
|
'credentials' => CredentialProvider::defaultProvider(),
|
||||||
|
]);
|
||||||
|
$client->registerStreamWrapper();
|
||||||
|
|
||||||
|
$zipFile = fopen("s3://$bucket/example.zip", 'w');
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
enableZip64: false,
|
||||||
|
outputStream: $zipFile,
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile(
|
||||||
|
fileName: 'file1.txt',
|
||||||
|
data: 'File1 data',
|
||||||
|
);
|
||||||
|
$zip->addFile(
|
||||||
|
fileName: 'file2.txt',
|
||||||
|
data: 'File2 data',
|
||||||
|
);
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
fclose($zipFile);
|
||||||
|
|
||||||
|
Stream to Callback Function
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
The CallbackStreamWrapper allows you to stream ZIP data to a custom callback function,
|
||||||
|
enabling flexible output handling such as streaming to multiple destinations,
|
||||||
|
progress tracking, or data transformation.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use ZipStream\ZipStream;
|
||||||
|
use ZipStream\Stream\CallbackStreamWrapper;
|
||||||
|
|
||||||
|
// Example 1: Stream to multiple destinations with proper file handling
|
||||||
|
$backupFile = fopen('backup.zip', 'wb');
|
||||||
|
$logFile = fopen('transfer.log', 'ab');
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: CallbackStreamWrapper::open(function (string $data) use ($backupFile, $logFile) {
|
||||||
|
// Send to browser
|
||||||
|
echo $data;
|
||||||
|
|
||||||
|
// Save to file efficiently
|
||||||
|
fwrite($backupFile, $data);
|
||||||
|
|
||||||
|
// Log transfer progress
|
||||||
|
fwrite($logFile, "Transferred " . strlen($data) . " bytes\n");
|
||||||
|
}),
|
||||||
|
sendHttpHeaders: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('hello.txt', 'Hello World!');
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
fclose($backupFile);
|
||||||
|
fclose($logFile);
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
// Example 2: Progress tracking
|
||||||
|
$totalBytes = 0;
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: CallbackStreamWrapper::open(function (string $data) use (&$totalBytes) {
|
||||||
|
$totalBytes += strlen($data);
|
||||||
|
reportProgress($totalBytes); // Report progress to your tracking system
|
||||||
|
|
||||||
|
// Your actual output handling
|
||||||
|
echo $data;
|
||||||
|
}),
|
||||||
|
sendHttpHeaders: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('large_file.txt', str_repeat('A', 10000));
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
// Example 3: Data transformation using PHP stream filters
|
||||||
|
// For data transformations, prefer PHP's built-in stream filters
|
||||||
|
$outputStream = fopen('php://output', 'w');
|
||||||
|
stream_filter_append($outputStream, 'convert.base64-encode');
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: $outputStream,
|
||||||
|
sendHttpHeaders: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('secret.txt', 'Confidential data');
|
||||||
|
$zip->finish();
|
||||||
|
fclose($outputStream);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
For data transformations, PHP's built-in stream filters are preferred over callback transformations. Stream filters operate at the stream level and maintain data integrity. You can register custom filters using ``stream_filter_register()`` for specialized transformations.
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
Usage with Symfony
|
||||||
|
===============
|
||||||
|
|
||||||
|
Overview for using ZipStream in Symfony
|
||||||
|
--------
|
||||||
|
|
||||||
|
Using ZipStream in Symfony requires use of Symfony's ``StreamedResponse`` when
|
||||||
|
used in controller actions.
|
||||||
|
|
||||||
|
Wrap your call to the relevant ``ZipStream`` stream method (i.e. ``addFile``,
|
||||||
|
``addFileFromPath``, ``addFileFromStream``) in Symfony's ``StreamedResponse``
|
||||||
|
function passing in any required arguments for your use case.
|
||||||
|
|
||||||
|
Using Symfony's ``StreamedResponse`` will allow Symfony to stream output from
|
||||||
|
ZipStream correctly to users' browsers and avoid a corrupted final zip landing
|
||||||
|
on the users' end.
|
||||||
|
|
||||||
|
Example for using ``ZipStream`` in a controller action to zip stream files
|
||||||
|
stored in an AWS S3 bucket by key:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use Aws\S3\S3Client;
|
||||||
|
use ZipStream;
|
||||||
|
|
||||||
|
//...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/zipstream", name="zipstream")
|
||||||
|
*/
|
||||||
|
public function zipStreamAction()
|
||||||
|
{
|
||||||
|
// sample test file on s3
|
||||||
|
$s3keys = array(
|
||||||
|
"ziptestfolder/file1.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
$s3Client = $this->get('app.amazon.s3'); //s3client service
|
||||||
|
$s3Client->registerStreamWrapper(); //required
|
||||||
|
|
||||||
|
// using StreamedResponse to wrap ZipStream functionality
|
||||||
|
// for files on AWS s3.
|
||||||
|
$response = new StreamedResponse(function() use($s3keys, $s3Client)
|
||||||
|
{
|
||||||
|
// Define suitable options for ZipStream Archive.
|
||||||
|
// this is needed to prevent issues with truncated zip files
|
||||||
|
//initialise zipstream with output zip filename and options.
|
||||||
|
$zip = new ZipStream\ZipStream(
|
||||||
|
outputName: 'test.zip',
|
||||||
|
defaultEnableZeroHeader: true,
|
||||||
|
contentType: 'application/octet-stream',
|
||||||
|
);
|
||||||
|
|
||||||
|
//loop keys - useful for multiple files
|
||||||
|
foreach ($s3keys as $key) {
|
||||||
|
// Get the file name in S3 key so we can save it to the zip
|
||||||
|
//file using the same name.
|
||||||
|
$fileName = basename($key);
|
||||||
|
|
||||||
|
// concatenate s3path.
|
||||||
|
// replace with your bucket name or get from parameters file.
|
||||||
|
$bucket = 'bucketname';
|
||||||
|
$s3path = "s3://" . $bucket . "/" . $key;
|
||||||
|
|
||||||
|
//addFileFromStream
|
||||||
|
if ($streamRead = fopen($s3path, 'r')) {
|
||||||
|
$zip->addFileFromStream(
|
||||||
|
fileName: $fileName,
|
||||||
|
stream: $streamRead,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
die('Could not open stream for reading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
In the above example, files on AWS S3 are being streamed from S3 to the Symfon
|
||||||
|
application via ``fopen`` call when the s3Client has ``registerStreamWrapper``
|
||||||
|
applied. This stream is then passed to ``ZipStream`` via the
|
||||||
|
``addFileFromStream`` function, which ZipStream then streams as a zip to the
|
||||||
|
client browser via Symfony's ``StreamedResponse``. No Zip is created server
|
||||||
|
side, which makes this approach a more efficient solution for streaming zips to
|
||||||
|
the client browser especially for larger files.
|
||||||
|
|
||||||
|
For the above use case you will need to have installed
|
||||||
|
`aws/aws-sdk-php-symfony <https://github.com/aws/aws-sdk-php-symfony>`_ to
|
||||||
|
support accessing S3 objects in your Symfony web application. This is not
|
||||||
|
required for locally stored files on you server you intend to stream via
|
||||||
|
``ZipStream``.
|
||||||
|
|
||||||
|
See official Symfony documentation for details on
|
||||||
|
`Symfony's StreamedResponse <https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response>`_
|
||||||
|
``Symfony\Component\HttpFoundation\StreamedResponse``.
|
||||||
|
|
||||||
|
Note from `S3 documentation <https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-stream-wrapper.html>`_:
|
||||||
|
|
||||||
|
Streams opened in "r" mode only allow data to be read from the stream, and
|
||||||
|
are not seekable by default. This is so that data can be downloaded from
|
||||||
|
Amazon S3 in a truly streaming manner, where previously read bytes do not
|
||||||
|
need to be buffered into memory. If you need a stream to be seekable, you
|
||||||
|
can pass seekable into the stream context options of a function.
|
||||||
|
|
||||||
|
Make sure to configure your S3 context correctly!
|
||||||
|
|
||||||
|
Uploading a file
|
||||||
|
--------
|
||||||
|
|
||||||
|
You need to add correct permissions
|
||||||
|
(see `#120 <https://github.com/maennchen/ZipStream-PHP/issues/120>`_)
|
||||||
|
|
||||||
|
**example code**
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
$path = "s3://{$adapter->getBucket()}/{$this->getArchivePath()}";
|
||||||
|
|
||||||
|
// the important bit
|
||||||
|
$outputContext = stream_context_create([
|
||||||
|
's3' => ['ACL' => 'public-read'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
fopen($path, 'w', null, $outputContext);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
Usage with Varnish
|
||||||
|
=============
|
||||||
|
|
||||||
|
Serving a big zip with varnish in between can cause random stream close.
|
||||||
|
This can be solved by adding attached code to the vcl file.
|
||||||
|
|
||||||
|
To avoid the problem, add the following to your varnish config file:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
sub vcl_recv {
|
||||||
|
# Varnish can’t intercept the discussion anymore
|
||||||
|
# helps for streaming big zips
|
||||||
|
if (req.url ~ "\.(tar|gz|zip|7z|exe)$") {
|
||||||
|
return (pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Varnish can’t intercept the discussion anymore
|
||||||
|
# helps for streaming big zips
|
||||||
|
sub vcl_pipe {
|
||||||
|
set bereq.http.connection = "close";
|
||||||
|
return (pipe);
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
ZipStream PHP
|
||||||
|
=============
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
index
|
||||||
|
Symfony
|
||||||
|
Options
|
||||||
|
StreamOutput
|
||||||
|
FlySystem
|
||||||
|
PSR7Streams
|
||||||
|
Nginx
|
||||||
|
Varnish
|
||||||
|
ContentLength
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. 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>`_
|
||||||
|
or run the following command:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
Your requirements could not be resolved to an installable set of packages.
|
||||||
|
|
||||||
|
Problem 1
|
||||||
|
- Root composer.json requires PHP extension ext-mbstring * but it is
|
||||||
|
missing from your system. Install or enable PHP's mbstrings extension.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
composer require symfony/polyfill-mbstring
|
||||||
|
|
||||||
|
Usage Intro
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Here's a simple example:
|
||||||
|
|
||||||
|
.. code-block:: 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
|
||||||
|
sendHttpHeaders: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create a file named 'hello.txt'
|
||||||
|
$zip->addFile(
|
||||||
|
fileName: 'hello.txt',
|
||||||
|
data: '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',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// finish the zip stream
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
You can also add comments, modify file timestamps, and customize (or
|
||||||
|
disable) the HTTP headers. It is also possible to specify the storage method
|
||||||
|
when adding files, the current default storage method is ``DEFLATE``
|
||||||
|
i.e files are stored with Compression mode 0x08.
|
||||||
|
|
||||||
|
Known Issues
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The native Mac OS archive extraction tool prior to macOS 10.15 might not open
|
||||||
|
archives in some conditions. A workaround is to disable the Zip64 feature with
|
||||||
|
the option ``enableZip64: false``. This limits the archive to 4 Gb and 64k files
|
||||||
|
but will allow users on macOS 10.14 and below to open them without issue.
|
||||||
|
See `#116 <https://github.com/maennchen/ZipStream-PHP/issues/116>`_.
|
||||||
|
|
||||||
|
The linux ``unzip`` utility might not handle properly unicode characters.
|
||||||
|
It is recommended to extract with another tool like
|
||||||
|
`7-zip <https://www.7-zip.org/>`_.
|
||||||
|
See `#146 <https://github.com/maennchen/ZipStream-PHP/issues/146>`_.
|
||||||
|
|
||||||
|
It is the responsibility of the client code to make sure that files are not
|
||||||
|
saved with the same path, as it is not possible for the library to figure it out
|
||||||
|
while streaming a zip.
|
||||||
|
See `#154 <https://github.com/maennchen/ZipStream-PHP/issues/154>`_.
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<phpdocumentor
|
||||||
|
configVersion="3"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="https://www.phpdoc.org"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/phpDocumentor/phpDocumentor/master/data/xsd/phpdoc.xsd"
|
||||||
|
>
|
||||||
|
<title>💾 ZipStream-PHP</title>
|
||||||
|
<paths>
|
||||||
|
<output>docs</output>
|
||||||
|
</paths>
|
||||||
|
<version number="3.0.0">
|
||||||
|
<folder>latest</folder>
|
||||||
|
<api>
|
||||||
|
<source dsn=".">
|
||||||
|
<path>src</path>
|
||||||
|
</source>
|
||||||
|
<output>api</output>
|
||||||
|
<ignore hidden="true" symlinks="true">
|
||||||
|
<path>tests/**/*</path>
|
||||||
|
<path>vendor/**/*</path>
|
||||||
|
</ignore>
|
||||||
|
<extensions>
|
||||||
|
<extension>php</extension>
|
||||||
|
</extensions>
|
||||||
|
<visibility>public</visibility>
|
||||||
|
<default-package-name>ZipStream</default-package-name>
|
||||||
|
<include-source>true</include-source>
|
||||||
|
</api>
|
||||||
|
<guide>
|
||||||
|
<source dsn=".">
|
||||||
|
<path>guides</path>
|
||||||
|
</source>
|
||||||
|
<output>guide</output>
|
||||||
|
</guide>
|
||||||
|
</version>
|
||||||
|
<setting name="guides.enabled" value="true"/>
|
||||||
|
<template name="default" />
|
||||||
|
</phpdocumentor>
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<?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/>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Application">
|
||||||
|
<directory>test</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<logging/>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?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"
|
||||||
|
phpVersion="8.2.0"
|
||||||
|
>
|
||||||
|
<!-- TODO: Update phpVersion when raising the minimum supported version -->
|
||||||
|
<projectFiles>
|
||||||
|
<directory name="src" />
|
||||||
|
<ignoreFiles>
|
||||||
|
<directory name="vendor" />
|
||||||
|
</ignoreFiles>
|
||||||
|
</projectFiles>
|
||||||
|
<issueHandlers>
|
||||||
|
<!-- Turn off dead code warnings for externally called functions -->
|
||||||
|
<PossiblyUnusedProperty errorLevel="suppress" />
|
||||||
|
<PossiblyUnusedMethod errorLevel="suppress" />
|
||||||
|
<PossiblyUnusedReturnValue errorLevel="suppress" />
|
||||||
|
</issueHandlers>
|
||||||
|
</psalm>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
abstract class Exception extends \Exception {}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a DOS time is overflowing
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a file wasn't found
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class FileNotFoundException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $path
|
||||||
|
) {
|
||||||
|
parent::__construct("The file with the path $path wasn't found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a file isn't readable
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class FileNotReadableException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $path
|
||||||
|
) {
|
||||||
|
parent::__construct("The file with the path $path isn't readable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?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.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a counter value exceeds storage size
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class OverflowException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a resource like `fread` returns false
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<?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.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Exception;
|
||||||
|
|
||||||
|
use ZipStream\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception gets invoked if a stream can't be read.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class StreamNotReadableException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('The stream could not be read.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class StreamNotSeekableException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('enableZeroHeader must be enable to add non seekable streams');
|
||||||
|
}
|
||||||
|
}
|
||||||
+430
@@ -0,0 +1,430 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use DeflateContext;
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipStream\Exception\FileSizeIncorrectException;
|
||||||
|
use ZipStream\Exception\OverflowException;
|
||||||
|
use ZipStream\Exception\ResourceActionException;
|
||||||
|
use ZipStream\Exception\SimulationFileUnknownException;
|
||||||
|
use ZipStream\Exception\StreamNotReadableException;
|
||||||
|
use ZipStream\Exception\StreamNotSeekableException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class File
|
||||||
|
{
|
||||||
|
private const CHUNKED_READ_BLOCK_SIZE = 0x1000000;
|
||||||
|
|
||||||
|
private Version $version;
|
||||||
|
|
||||||
|
private int $compressedSize = 0;
|
||||||
|
|
||||||
|
private int $uncompressedSize = 0;
|
||||||
|
|
||||||
|
private int $crc = 0;
|
||||||
|
|
||||||
|
private int $generalPurposeBitFlag = 0;
|
||||||
|
|
||||||
|
private readonly string $fileName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var resource|null
|
||||||
|
*/
|
||||||
|
private $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Closure $dataCallback
|
||||||
|
* @psalm-param Closure(): resource $dataCallback
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
if ($this->enableZeroHeader) {
|
||||||
|
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cloneSimulationExecution(): self
|
||||||
|
{
|
||||||
|
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 !== null) {
|
||||||
|
$this->uncompressedSize = $forecastSize;
|
||||||
|
$this->compressedSize = $forecastSize;
|
||||||
|
} else {
|
||||||
|
$this->readStream(send: false);
|
||||||
|
if (rewind($this->unpackStream()) === false) {
|
||||||
|
throw new ResourceActionException('rewind', $this->unpackStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFileHeader();
|
||||||
|
|
||||||
|
$detectedSize = $forecastSize ?? ($this->compressedSize > 0 ? $this->compressedSize : null);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$this->isSimulation() &&
|
||||||
|
$detectedSize !== null
|
||||||
|
) {
|
||||||
|
$this->uncompressedSize = $detectedSize;
|
||||||
|
$this->compressedSize = $detectedSize;
|
||||||
|
($this->recordSentBytes)($detectedSize);
|
||||||
|
} else {
|
||||||
|
$this->readStream(send: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 !== null) {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
private function addFileHeader(): void
|
||||||
|
{
|
||||||
|
$forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64;
|
||||||
|
|
||||||
|
$footer = $this->buildZip64ExtraBlock($forceEnableZip64);
|
||||||
|
|
||||||
|
$zip64Enabled = $footer !== '';
|
||||||
|
|
||||||
|
if ($zip64Enabled) {
|
||||||
|
$this->version = Version::ZIP64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) {
|
||||||
|
// Put the tricky entry to
|
||||||
|
// force Linux unzip to lookup EFS flag.
|
||||||
|
$footer .= Zs\ExtendedInformationExtraField::generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
($this->send)($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip characters that are not legal in Windows filenames
|
||||||
|
* to prevent compatibility issues
|
||||||
|
*/
|
||||||
|
private static function filterFilename(
|
||||||
|
/**
|
||||||
|
* Unprocessed filename
|
||||||
|
*/
|
||||||
|
string $fileName
|
||||||
|
): string {
|
||||||
|
// strip leading slashes from file name
|
||||||
|
// (fixes bug in windows archive viewer)
|
||||||
|
$fileName = ltrim($fileName, '/');
|
||||||
|
|
||||||
|
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkEncoding(): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$footer = DataDescriptor::generate(
|
||||||
|
crc32UncompressedData: $this->crc,
|
||||||
|
compressedSize: $this->compressedSize,
|
||||||
|
uncompressedSize: $this->uncompressedSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
($this->send)($footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readStream(bool $send): void
|
||||||
|
{
|
||||||
|
$this->compressedSize = 0;
|
||||||
|
$this->uncompressedSize = 0;
|
||||||
|
$hash = hash_init('crc32b');
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
throw new ResourceActionException('fread', $this->unpackStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
hash_update($hash, $data);
|
||||||
|
|
||||||
|
$this->uncompressedSize += strlen($data);
|
||||||
|
|
||||||
|
if ($deflate) {
|
||||||
|
$data = deflate_add(
|
||||||
|
$deflate,
|
||||||
|
$data,
|
||||||
|
feof($this->unpackStream()) ? ZLIB_FINISH : ZLIB_NO_FLUSH
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
throw new RuntimeException('deflate_add failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->compressedSize += strlen($data);
|
||||||
|
|
||||||
|
if ($send) {
|
||||||
|
($this->send)($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->exactSize !== null && $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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCdrFile(): string
|
||||||
|
{
|
||||||
|
$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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSimulation(): bool
|
||||||
|
{
|
||||||
|
return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZipStream execution operation modes
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* TODO: Make class readonly when requiring PHP 8.2 exclusively
|
||||||
|
*/
|
||||||
|
final 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Stream;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper that allows writing data to a callback function.
|
||||||
|
*
|
||||||
|
* This wrapper creates a virtual stream that forwards all written data
|
||||||
|
* to a provided callback function, enabling custom output handling
|
||||||
|
* such as streaming to HTTP responses, files, or other destinations.
|
||||||
|
*
|
||||||
|
* @psalm-suppress UnusedClass Used dynamically through stream_wrapper_register
|
||||||
|
*/
|
||||||
|
final class CallbackStreamWrapper
|
||||||
|
{
|
||||||
|
public const PROTOCOL = 'zipcb';
|
||||||
|
|
||||||
|
/** @var resource|null */
|
||||||
|
public $context;
|
||||||
|
|
||||||
|
/** @var array<string, callable(string):void> Map of stream IDs to callback functions */
|
||||||
|
private static array $callbacks = [];
|
||||||
|
|
||||||
|
/** @var string|null Unique identifier for this stream instance */
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
/** @var int Current position in the stream */
|
||||||
|
private int $pos = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destructor - ensures cleanup even if stream_close() isn't called.
|
||||||
|
* Prevents memory leaks in long-running processes.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->stream_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new callback stream.
|
||||||
|
*
|
||||||
|
* @param callable(string):void $callback Function to call with written data
|
||||||
|
* @return resource|false Stream resource or false on failure
|
||||||
|
*/
|
||||||
|
public static function open(callable $callback)
|
||||||
|
{
|
||||||
|
if (!in_array(self::PROTOCOL, stream_get_wrappers(), true)) {
|
||||||
|
if (!stream_wrapper_register(self::PROTOCOL, self::class)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cryptographically secure unique ID to prevent collisions
|
||||||
|
$id = 'cb_' . bin2hex(random_bytes(16));
|
||||||
|
self::$callbacks[$id] = $callback;
|
||||||
|
|
||||||
|
return fopen(self::PROTOCOL . "://{$id}", 'wb');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all registered callbacks (useful for testing).
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public static function cleanup(): void
|
||||||
|
{
|
||||||
|
self::$callbacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the stream.
|
||||||
|
*
|
||||||
|
* @param string $path Stream path containing the callback ID
|
||||||
|
* @param string $mode File mode (must contain 'w' for writing)
|
||||||
|
* @param int $options Stream options (required by interface, unused)
|
||||||
|
* @param string|null $opened_path Opened path reference (required by interface, unused)
|
||||||
|
* @return bool True if stream opened successfully
|
||||||
|
* @psalm-suppress UnusedParam $options and $opened_path are required by the stream wrapper interface
|
||||||
|
*/
|
||||||
|
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
||||||
|
{
|
||||||
|
if (!str_contains($mode, 'w')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = parse_url($path, PHP_URL_HOST);
|
||||||
|
if ($host === false || $host === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->id = $host;
|
||||||
|
return isset(self::$callbacks[$this->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to the callback.
|
||||||
|
*
|
||||||
|
* @param string $data Data to write
|
||||||
|
* @return int Number of bytes written
|
||||||
|
* @throws RuntimeException If callback execution fails
|
||||||
|
*/
|
||||||
|
public function stream_write(string $data): int
|
||||||
|
{
|
||||||
|
if ($this->id === null) {
|
||||||
|
trigger_error('Stream not properly initialized', E_USER_WARNING);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$callback = self::$callbacks[$this->id] ?? null;
|
||||||
|
if ($callback === null) {
|
||||||
|
trigger_error('Callback not found for stream', E_USER_WARNING);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$callback($data);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Callback function failed during stream write: ' . $e->getMessage(),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($data);
|
||||||
|
$this->pos += $length;
|
||||||
|
return $length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current position in stream.
|
||||||
|
*
|
||||||
|
* @return int Current position
|
||||||
|
*/
|
||||||
|
public function stream_tell(): int
|
||||||
|
{
|
||||||
|
return $this->pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if stream has reached end of file.
|
||||||
|
*
|
||||||
|
* @return bool Always false for write-only streams
|
||||||
|
*/
|
||||||
|
public function stream_eof(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush stream buffers.
|
||||||
|
*
|
||||||
|
* @return bool Always true (no buffering)
|
||||||
|
*/
|
||||||
|
public function stream_flush(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the stream and clean up callback.
|
||||||
|
*/
|
||||||
|
public function stream_close(): void
|
||||||
|
{
|
||||||
|
if ($this->id !== null) {
|
||||||
|
unset(self::$callbacks[$this->id]);
|
||||||
|
$this->id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stream statistics.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Stream statistics
|
||||||
|
*/
|
||||||
|
public function stream_stat(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dev' => 0,
|
||||||
|
'ino' => 0,
|
||||||
|
'mode' => 0o100666, // Regular file, read/write permissions
|
||||||
|
'nlink' => 1,
|
||||||
|
'uid' => 0,
|
||||||
|
'gid' => 0,
|
||||||
|
'rdev' => 0,
|
||||||
|
'size' => $this->pos,
|
||||||
|
'atime' => time(),
|
||||||
|
'mtime' => time(),
|
||||||
|
'ctime' => time(),
|
||||||
|
'blksize' => 4096,
|
||||||
|
'blocks' => ceil($this->pos / 4096),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from stream (not supported - write-only stream).
|
||||||
|
*
|
||||||
|
* @param int $count Number of bytes to read (required by interface, unused)
|
||||||
|
* @return string Always empty string
|
||||||
|
* @psalm-suppress UnusedParam $count is required by the stream wrapper interface
|
||||||
|
*/
|
||||||
|
public function stream_read(int $count): string
|
||||||
|
{
|
||||||
|
trigger_error('Read operations not supported on callback streams', E_USER_WARNING);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to position in stream (not supported).
|
||||||
|
*
|
||||||
|
* @param int $offset Offset to seek to (required by interface, unused)
|
||||||
|
* @param int $whence Seek mode (required by interface, unused)
|
||||||
|
* @return bool Always false
|
||||||
|
* @psalm-suppress UnusedParam $offset and $whence are required by the stream wrapper interface
|
||||||
|
*/
|
||||||
|
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
|
||||||
|
{
|
||||||
|
trigger_error('Seek operations not supported on callback streams', E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set options on stream (not supported).
|
||||||
|
*
|
||||||
|
* @param int $option Option to set (required by interface, unused)
|
||||||
|
* @param int $arg1 First argument (required by interface, unused)
|
||||||
|
* @param int $arg2 Second argument (required by interface, unused)
|
||||||
|
* @return bool Always false
|
||||||
|
* @psalm-suppress UnusedParam All parameters are required by the stream wrapper interface
|
||||||
|
*/
|
||||||
|
public function stream_set_option(int $option, int $arg1, int $arg2): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate stream (not supported).
|
||||||
|
*
|
||||||
|
* @param int $new_size New size (required by interface, unused)
|
||||||
|
* @return bool Always false
|
||||||
|
* @psalm-suppress UnusedParam $new_size is required by the stream wrapper interface
|
||||||
|
*/
|
||||||
|
public function stream_truncate(int $new_size): bool
|
||||||
|
{
|
||||||
|
trigger_error('Truncate operations not supported on callback streams', E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
<?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, $month, $day, $hour, $minute, $second] = explode(' ', $dateTime->format('Y n j G i s'));
|
||||||
|
|
||||||
|
return
|
||||||
|
((int) $year << 25) |
|
||||||
|
((int) $month << 21) |
|
||||||
|
((int) $day << 16) |
|
||||||
|
((int) $hour << 11) |
|
||||||
|
((int) $minute << 5) |
|
||||||
|
((int) $second >> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
enum Version: int
|
||||||
|
{
|
||||||
|
case STORE = 0x000A; // 1.00
|
||||||
|
case DEFLATE = 0x0014; // 2.00
|
||||||
|
case ZIP64 = 0x002D; // 4.50
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?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),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+873
@@ -0,0 +1,873 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use GuzzleHttp\Psr7\StreamWrapper;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipStream\Exception\FileNotFoundException;
|
||||||
|
use ZipStream\Exception\FileNotReadableException;
|
||||||
|
use ZipStream\Exception\OverflowException;
|
||||||
|
use ZipStream\Exception\ResourceActionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streamed, dynamically generated zip archives.
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
*
|
||||||
|
* Streaming zip archives is a simple, three-step process:
|
||||||
|
*
|
||||||
|
* 1. Create the zip stream:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $zip = new ZipStream(outputName: 'example.zip');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 2. Add one or more files to the archive:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // add first file
|
||||||
|
* $zip->addFile(fileName: 'world.txt', data: 'Hello World');
|
||||||
|
*
|
||||||
|
* // add second file
|
||||||
|
* $zip->addFile(fileName: 'moon.txt', data: 'Hello Moon');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 3. Finish the zip stream:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $zip->finish();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can also add an archive comment, add comments to individual files,
|
||||||
|
* and adjust the timestamp of files. See the API documentation for each
|
||||||
|
* method below for additional information.
|
||||||
|
*
|
||||||
|
* ## Example
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // create a new zip stream object
|
||||||
|
* $zip = new ZipStream(outputName: 'some_files.zip');
|
||||||
|
*
|
||||||
|
* // list of local files
|
||||||
|
* $files = array('foo.txt', 'bar.jpg');
|
||||||
|
*
|
||||||
|
* // read and add each file to the archive
|
||||||
|
* foreach ($files as $path)
|
||||||
|
* $zip->addFileFromPath(fileName: $path, $path);
|
||||||
|
*
|
||||||
|
* // write archive footer to stream
|
||||||
|
* $zip->finish();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
class ZipStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This number corresponds to the ZIP version/OS used (2 bytes)
|
||||||
|
* From: https://www.iana.org/assignments/media-types/application/zip
|
||||||
|
* The upper byte (leftmost one) indicates the host system (OS) for the
|
||||||
|
* file. Software can use this information to determine
|
||||||
|
* the line record format for text files etc. The current
|
||||||
|
* mappings are:
|
||||||
|
*
|
||||||
|
* 0 - MS-DOS and OS/2 (F.A.T. file systems)
|
||||||
|
* 1 - Amiga 2 - VAX/VMS
|
||||||
|
* 3 - *nix 4 - VM/CMS
|
||||||
|
* 5 - Atari ST 6 - OS/2 H.P.F.S.
|
||||||
|
* 7 - Macintosh 8 - Z-System
|
||||||
|
* 9 - CP/M 10 thru 255 - unused
|
||||||
|
*
|
||||||
|
* The lower byte (rightmost one) indicates the version number of the
|
||||||
|
* software used to encode the file. The value/10
|
||||||
|
* indicates the major version number, and the value
|
||||||
|
* mod 10 is the minor version number.
|
||||||
|
* Here we are using 6 for the OS, indicating OS/2 H.P.F.S.
|
||||||
|
* to prevent file permissions issues upon extract (see #84)
|
||||||
|
* 0x603 is 00000110 00000011 in binary, so 6 and 3
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public const ZIP_VERSION_MADE_BY = 0x603;
|
||||||
|
|
||||||
|
private bool $ready = true;
|
||||||
|
|
||||||
|
private int $offset = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private array $centralDirectoryRecords = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
private $outputStream;
|
||||||
|
|
||||||
|
private readonly Closure $httpHeaderCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var File[]
|
||||||
|
*/
|
||||||
|
private array $recordedSimulation = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ZipStream object.
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // create a new zip file named 'foo.zip'
|
||||||
|
* $zip = new ZipStream(outputName: 'foo.zip');
|
||||||
|
*
|
||||||
|
* // create a new zip file named 'bar.zip' with a comment
|
||||||
|
* $zip = new ZipStream(
|
||||||
|
* outputName: 'bar.zip',
|
||||||
|
* comment: 'this is a comment for the zip file.',
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param OperationMode $operationMode
|
||||||
|
* The mode can be used to switch between `NORMAL` and `SIMULATION_*` modes.
|
||||||
|
* For details see the `OperationMode` documentation.
|
||||||
|
*
|
||||||
|
* Default to `NORMAL`.
|
||||||
|
*
|
||||||
|
* @param string $comment
|
||||||
|
* Archive Level Comment
|
||||||
|
*
|
||||||
|
* @param StreamInterface|resource|null $outputStream
|
||||||
|
* Override the output of the archive to a different target.
|
||||||
|
*
|
||||||
|
* By default the archive is sent to `STDOUT`.
|
||||||
|
*
|
||||||
|
* @param CompressionMethod $defaultCompressionMethod
|
||||||
|
* How to handle file compression. Legal values are
|
||||||
|
* `CompressionMethod::DEFLATE` (the default), or
|
||||||
|
* `CompressionMethod::STORE`. `STORE` sends the file raw and is
|
||||||
|
* significantly faster, while `DEFLATE` compresses the file and
|
||||||
|
* is much, much slower.
|
||||||
|
*
|
||||||
|
* @param int $defaultDeflateLevel
|
||||||
|
* Default deflation level. Only relevant if `compressionMethod`
|
||||||
|
* is `DEFLATE`.
|
||||||
|
*
|
||||||
|
* See details of [`deflate_init`](https://www.php.net/manual/en/function.deflate-init.php#refsect1-function.deflate-init-parameters)
|
||||||
|
*
|
||||||
|
* @param bool $enableZip64
|
||||||
|
* Enable Zip64 extension, supporting very large
|
||||||
|
* archives (any size > 4 GB or file count > 64k)
|
||||||
|
*
|
||||||
|
* @param bool $defaultEnableZeroHeader
|
||||||
|
* Enable streaming files with single read.
|
||||||
|
*
|
||||||
|
* When the zero header is set, the file is streamed into the output
|
||||||
|
* and the size & checksum are added at the end of the file. This is the
|
||||||
|
* fastest method and uses the least memory. Unfortunately not all
|
||||||
|
* ZIP clients fully support this and can lead to clients reporting
|
||||||
|
* the generated ZIP files as corrupted in combination with other
|
||||||
|
* circumstances. (Zip64 enabled, using UTF8 in comments / names etc.)
|
||||||
|
*
|
||||||
|
* When the zero header is not set, the length & checksum need to be
|
||||||
|
* defined before the file is actually added. To prevent loading all
|
||||||
|
* the data into memory, the data has to be read twice. If the data
|
||||||
|
* which is added is not seekable, this call will fail.
|
||||||
|
*
|
||||||
|
* @param bool $sendHttpHeaders
|
||||||
|
* Boolean indicating whether or not to send
|
||||||
|
* the HTTP headers for this file.
|
||||||
|
*
|
||||||
|
* @param ?Closure $httpHeaderCallback
|
||||||
|
* The method called to send HTTP headers
|
||||||
|
*
|
||||||
|
* @param string|null $outputName
|
||||||
|
* The name of the created archive.
|
||||||
|
*
|
||||||
|
* Only relevant if `$sendHttpHeaders = true`.
|
||||||
|
*
|
||||||
|
* @param string $contentDisposition
|
||||||
|
* HTTP Content-Disposition
|
||||||
|
*
|
||||||
|
* Only relevant if `sendHttpHeaders = true`.
|
||||||
|
*
|
||||||
|
* @param string $contentType
|
||||||
|
* HTTP Content Type
|
||||||
|
*
|
||||||
|
* Only relevant if `sendHttpHeaders = true`.
|
||||||
|
*
|
||||||
|
* @param bool $flushOutput
|
||||||
|
* Enable flush after every write to output stream.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private OperationMode $operationMode = OperationMode::NORMAL,
|
||||||
|
private readonly string $comment = '',
|
||||||
|
$outputStream = null,
|
||||||
|
private readonly CompressionMethod $defaultCompressionMethod = CompressionMethod::DEFLATE,
|
||||||
|
private readonly int $defaultDeflateLevel = 6,
|
||||||
|
private readonly bool $enableZip64 = true,
|
||||||
|
private readonly bool $defaultEnableZeroHeader = true,
|
||||||
|
private bool $sendHttpHeaders = true,
|
||||||
|
?Closure $httpHeaderCallback = null,
|
||||||
|
private readonly ?string $outputName = null,
|
||||||
|
private readonly string $contentDisposition = 'attachment',
|
||||||
|
private readonly string $contentType = 'application/x-zip',
|
||||||
|
private bool $flushOutput = false,
|
||||||
|
) {
|
||||||
|
$this->outputStream = self::normalizeStream($outputStream);
|
||||||
|
$this->httpHeaderCallback = $httpHeaderCallback ?? header(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file to the archive.
|
||||||
|
*
|
||||||
|
* ##### File Options
|
||||||
|
*
|
||||||
|
* See {@see addFileFromPsr7Stream()}
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // add a file named 'world.txt'
|
||||||
|
* $zip->addFile(fileName: 'world.txt', data: 'Hello World!');
|
||||||
|
*
|
||||||
|
* // add a file named 'bar.jpg' with a comment and a last-modified
|
||||||
|
* // time of two hours ago
|
||||||
|
* $zip->addFile(
|
||||||
|
* fileName: 'bar.jpg',
|
||||||
|
* data: $data,
|
||||||
|
* comment: 'this is a comment about bar.jpg',
|
||||||
|
* lastModificationDateTime: new DateTime('2 hours ago'),
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* contents of file
|
||||||
|
*/
|
||||||
|
public function addFile(
|
||||||
|
string $fileName,
|
||||||
|
string $data,
|
||||||
|
string $comment = '',
|
||||||
|
?CompressionMethod $compressionMethod = null,
|
||||||
|
?int $deflateLevel = null,
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
?int $maxSize = null,
|
||||||
|
?int $exactSize = null,
|
||||||
|
?bool $enableZeroHeader = null,
|
||||||
|
): void {
|
||||||
|
$this->addFileFromCallback(
|
||||||
|
fileName: $fileName,
|
||||||
|
callback: fn() => $data,
|
||||||
|
comment: $comment,
|
||||||
|
compressionMethod: $compressionMethod,
|
||||||
|
deflateLevel: $deflateLevel,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime,
|
||||||
|
maxSize: $maxSize,
|
||||||
|
exactSize: $exactSize,
|
||||||
|
enableZeroHeader: $enableZeroHeader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file at path to the archive.
|
||||||
|
*
|
||||||
|
* ##### File Options
|
||||||
|
*
|
||||||
|
* See {@see addFileFromPsr7Stream()}
|
||||||
|
*
|
||||||
|
* ###### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // add a file named 'foo.txt' from the local file '/tmp/foo.txt'
|
||||||
|
* $zip->addFileFromPath(
|
||||||
|
* fileName: 'foo.txt',
|
||||||
|
* path: '/tmp/foo.txt',
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // add a file named 'bigfile.rar' from the local file
|
||||||
|
* // '/usr/share/bigfile.rar' with a comment and a last-modified
|
||||||
|
* // time of two hours ago
|
||||||
|
* $zip->addFileFromPath(
|
||||||
|
* fileName: 'bigfile.rar',
|
||||||
|
* path: '/usr/share/bigfile.rar',
|
||||||
|
* comment: 'this is a comment about bigfile.rar',
|
||||||
|
* lastModificationDateTime: new DateTime('2 hours ago'),
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @throws \ZipStream\Exception\FileNotFoundException
|
||||||
|
* @throws \ZipStream\Exception\FileNotReadableException
|
||||||
|
*/
|
||||||
|
public function addFileFromPath(
|
||||||
|
/**
|
||||||
|
* name of file in archive (including directory path).
|
||||||
|
*/
|
||||||
|
string $fileName,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* path to file on disk (note: paths should be encoded using
|
||||||
|
* UNIX-style forward slashes -- e.g '/path/to/some/file').
|
||||||
|
*/
|
||||||
|
string $path,
|
||||||
|
string $comment = '',
|
||||||
|
?CompressionMethod $compressionMethod = null,
|
||||||
|
?int $deflateLevel = null,
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
?int $maxSize = null,
|
||||||
|
?int $exactSize = null,
|
||||||
|
?bool $enableZeroHeader = null,
|
||||||
|
): void {
|
||||||
|
if (!is_readable($path)) {
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
throw new FileNotFoundException($path);
|
||||||
|
}
|
||||||
|
throw new FileNotReadableException($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileTime = filemtime($path);
|
||||||
|
if ($fileTime !== false) {
|
||||||
|
$lastModificationDateTime ??= (new DateTimeImmutable())->setTimestamp($fileTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFileFromCallback(
|
||||||
|
fileName: $fileName,
|
||||||
|
callback: function () use ($path) {
|
||||||
|
|
||||||
|
$stream = fopen($path, 'rb');
|
||||||
|
|
||||||
|
if (!$stream) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new ResourceActionException('fopen');
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
},
|
||||||
|
comment: $comment,
|
||||||
|
compressionMethod: $compressionMethod,
|
||||||
|
deflateLevel: $deflateLevel,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime,
|
||||||
|
maxSize: $maxSize,
|
||||||
|
exactSize: $exactSize,
|
||||||
|
enableZeroHeader: $enableZeroHeader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an open stream (resource) to the archive.
|
||||||
|
*
|
||||||
|
* ##### File Options
|
||||||
|
*
|
||||||
|
* See {@see addFileFromPsr7Stream()}
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // create a temporary file stream and write text to it
|
||||||
|
* $filePointer = tmpfile();
|
||||||
|
* fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.');
|
||||||
|
*
|
||||||
|
* // add a file named 'streamfile.txt' from the content of the stream
|
||||||
|
* $archive->addFileFromStream(
|
||||||
|
* fileName: 'streamfile.txt',
|
||||||
|
* stream: $filePointer,
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param resource $stream contents of file as a stream resource
|
||||||
|
*/
|
||||||
|
public function addFileFromStream(
|
||||||
|
string $fileName,
|
||||||
|
$stream,
|
||||||
|
string $comment = '',
|
||||||
|
?CompressionMethod $compressionMethod = null,
|
||||||
|
?int $deflateLevel = null,
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
?int $maxSize = null,
|
||||||
|
?int $exactSize = null,
|
||||||
|
?bool $enableZeroHeader = null,
|
||||||
|
): void {
|
||||||
|
$this->addFileFromCallback(
|
||||||
|
fileName: $fileName,
|
||||||
|
callback: fn() => $stream,
|
||||||
|
comment: $comment,
|
||||||
|
compressionMethod: $compressionMethod,
|
||||||
|
deflateLevel: $deflateLevel,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime,
|
||||||
|
maxSize: $maxSize,
|
||||||
|
exactSize: $exactSize,
|
||||||
|
enableZeroHeader: $enableZeroHeader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an open stream to the archive.
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $stream = $response->getBody();
|
||||||
|
* // add a file named 'streamfile.txt' from the content of the stream
|
||||||
|
* $archive->addFileFromPsr7Stream(
|
||||||
|
* fileName: 'streamfile.txt',
|
||||||
|
* stream: $stream,
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param string $fileName
|
||||||
|
* path of file in archive (including directory)
|
||||||
|
*
|
||||||
|
* @param StreamInterface $stream
|
||||||
|
* contents of file as a stream resource
|
||||||
|
*
|
||||||
|
* @param string $comment
|
||||||
|
* ZIP comment for this file
|
||||||
|
*
|
||||||
|
* @param ?CompressionMethod $compressionMethod
|
||||||
|
* Override `defaultCompressionMethod`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*
|
||||||
|
* @param ?int $deflateLevel
|
||||||
|
* Override `defaultDeflateLevel`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*
|
||||||
|
* @param ?DateTimeInterface $lastModificationDateTime
|
||||||
|
* Set last modification time of file.
|
||||||
|
*
|
||||||
|
* Default: `now`
|
||||||
|
*
|
||||||
|
* @param ?int $maxSize
|
||||||
|
* Only read `maxSize` bytes from file.
|
||||||
|
*
|
||||||
|
* The file is considered done when either reaching `EOF`
|
||||||
|
* or the `maxSize`.
|
||||||
|
*
|
||||||
|
* @param ?int $exactSize
|
||||||
|
* Read exactly `exactSize` bytes from file.
|
||||||
|
* If `EOF` is reached before reading `exactSize` bytes, an error will be
|
||||||
|
* thrown. The parameter allows for faster size calculations if the `stream`
|
||||||
|
* does not support `fstat` size or is slow and otherwise known beforehand.
|
||||||
|
*
|
||||||
|
* @param ?bool $enableZeroHeader
|
||||||
|
* Override `defaultEnableZeroHeader`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*/
|
||||||
|
public function addFileFromPsr7Stream(
|
||||||
|
string $fileName,
|
||||||
|
StreamInterface $stream,
|
||||||
|
string $comment = '',
|
||||||
|
?CompressionMethod $compressionMethod = null,
|
||||||
|
?int $deflateLevel = null,
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
?int $maxSize = null,
|
||||||
|
?int $exactSize = null,
|
||||||
|
?bool $enableZeroHeader = null,
|
||||||
|
): void {
|
||||||
|
$this->addFileFromCallback(
|
||||||
|
fileName: $fileName,
|
||||||
|
callback: fn() => $stream,
|
||||||
|
comment: $comment,
|
||||||
|
compressionMethod: $compressionMethod,
|
||||||
|
deflateLevel: $deflateLevel,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime,
|
||||||
|
maxSize: $maxSize,
|
||||||
|
exactSize: $exactSize,
|
||||||
|
enableZeroHeader: $enableZeroHeader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file based on a callback.
|
||||||
|
*
|
||||||
|
* This is useful when you want to simulate a lot of files without keeping
|
||||||
|
* all of the file handles open at the same time.
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* foreach($files as $name => $size) {
|
||||||
|
* $archive->addFileFromCallback(
|
||||||
|
* fileName: 'streamfile.txt',
|
||||||
|
* exactSize: $size,
|
||||||
|
* callback: function() use($name): Psr\Http\Message\StreamInterface {
|
||||||
|
* $response = download($name);
|
||||||
|
* return $response->getBody();
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param string $fileName
|
||||||
|
* path of file in archive (including directory)
|
||||||
|
*
|
||||||
|
* @param Closure $callback
|
||||||
|
* @psalm-param Closure(): (resource|StreamInterface|string) $callback
|
||||||
|
* A callback to get the file contents in the shape of a PHP stream,
|
||||||
|
* a Psr StreamInterface implementation, or a string.
|
||||||
|
*
|
||||||
|
* @param string $comment
|
||||||
|
* ZIP comment for this file
|
||||||
|
*
|
||||||
|
* @param ?CompressionMethod $compressionMethod
|
||||||
|
* Override `defaultCompressionMethod`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*
|
||||||
|
* @param ?int $deflateLevel
|
||||||
|
* Override `defaultDeflateLevel`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*
|
||||||
|
* @param ?DateTimeInterface $lastModificationDateTime
|
||||||
|
* Set last modification time of file.
|
||||||
|
*
|
||||||
|
* Default: `now`
|
||||||
|
*
|
||||||
|
* @param ?int $maxSize
|
||||||
|
* Only read `maxSize` bytes from file.
|
||||||
|
*
|
||||||
|
* The file is considered done when either reaching `EOF`
|
||||||
|
* or the `maxSize`.
|
||||||
|
*
|
||||||
|
* @param ?int $exactSize
|
||||||
|
* Read exactly `exactSize` bytes from file.
|
||||||
|
* If `EOF` is reached before reading `exactSize` bytes, an error will be
|
||||||
|
* thrown. The parameter allows for faster size calculations if the `stream`
|
||||||
|
* does not support `fstat` size or is slow and otherwise known beforehand.
|
||||||
|
*
|
||||||
|
* @param ?bool $enableZeroHeader
|
||||||
|
* Override `defaultEnableZeroHeader`
|
||||||
|
*
|
||||||
|
* See {@see __construct()}
|
||||||
|
*/
|
||||||
|
public function addFileFromCallback(
|
||||||
|
string $fileName,
|
||||||
|
Closure $callback,
|
||||||
|
string $comment = '',
|
||||||
|
?CompressionMethod $compressionMethod = null,
|
||||||
|
?int $deflateLevel = null,
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
?int $maxSize = null,
|
||||||
|
?int $exactSize = null,
|
||||||
|
?bool $enableZeroHeader = null,
|
||||||
|
): void {
|
||||||
|
$file = new File(
|
||||||
|
dataCallback: function () use ($callback, $maxSize) {
|
||||||
|
$data = $callback();
|
||||||
|
|
||||||
|
if (is_resource($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data instanceof StreamInterface) {
|
||||||
|
return StreamWrapper::getResource($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stream = fopen('php://memory', 'rw+');
|
||||||
|
if ($stream === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new ResourceActionException('fopen');
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
if ($maxSize !== null && fwrite($stream, $data, $maxSize) === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new ResourceActionException('fwrite', $stream);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
} elseif (fwrite($stream, $data) === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new ResourceActionException('fwrite', $stream);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
if (rewind($stream) === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new ResourceActionException('rewind', $stream);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
|
||||||
|
},
|
||||||
|
send: $this->send(...),
|
||||||
|
recordSentBytes: $this->recordSentBytes(...),
|
||||||
|
operationMode: $this->operationMode,
|
||||||
|
fileName: $fileName,
|
||||||
|
startOffset: $this->offset,
|
||||||
|
compressionMethod: $compressionMethod ?? $this->defaultCompressionMethod,
|
||||||
|
comment: $comment,
|
||||||
|
deflateLevel: $deflateLevel ?? $this->defaultDeflateLevel,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime ?? new DateTimeImmutable(),
|
||||||
|
maxSize: $maxSize,
|
||||||
|
exactSize: $exactSize,
|
||||||
|
enableZip64: $this->enableZip64,
|
||||||
|
enableZeroHeader: $enableZeroHeader ?? $this->defaultEnableZeroHeader,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->operationMode !== OperationMode::NORMAL) {
|
||||||
|
$this->recordedSimulation[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->centralDirectoryRecords[] = $file->process();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a directory to the archive.
|
||||||
|
*
|
||||||
|
* ##### File Options
|
||||||
|
*
|
||||||
|
* See {@see addFileFromPsr7Stream()}
|
||||||
|
*
|
||||||
|
* ##### Examples
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // add a directory named 'world/'
|
||||||
|
* $zip->addDirectory(fileName: 'world/');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public function addDirectory(
|
||||||
|
string $fileName,
|
||||||
|
string $comment = '',
|
||||||
|
?DateTimeInterface $lastModificationDateTime = null,
|
||||||
|
): void {
|
||||||
|
if (!str_ends_with($fileName, '/')) {
|
||||||
|
$fileName .= '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
fileName: $fileName,
|
||||||
|
data: '',
|
||||||
|
comment: $comment,
|
||||||
|
compressionMethod: CompressionMethod::STORE,
|
||||||
|
deflateLevel: null,
|
||||||
|
lastModificationDateTime: $lastModificationDateTime,
|
||||||
|
maxSize: 0,
|
||||||
|
exactSize: 0,
|
||||||
|
enableZeroHeader: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a previously calculated simulation.
|
||||||
|
*
|
||||||
|
* ##### Example
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $zip = new ZipStream(
|
||||||
|
* outputName: 'foo.zip',
|
||||||
|
* operationMode: OperationMode::SIMULATE_STRICT,
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* $zip->addFile('test.txt', 'Hello World');
|
||||||
|
*
|
||||||
|
* $size = $zip->finish();
|
||||||
|
*
|
||||||
|
* header('Content-Length: '. $size);
|
||||||
|
*
|
||||||
|
* $zip->executeSimulation();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public function executeSimulation(): void
|
||||||
|
{
|
||||||
|
if ($this->operationMode !== OperationMode::NORMAL) {
|
||||||
|
throw new RuntimeException('Zip simulation is not finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->recordedSimulation as $file) {
|
||||||
|
$this->centralDirectoryRecords[] = $file->cloneSimulationExecution()->process();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write zip footer to stream.
|
||||||
|
*
|
||||||
|
* The class is left in an unusable state after `finish`.
|
||||||
|
*
|
||||||
|
* ##### Example
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // write footer to stream
|
||||||
|
* $zip->finish();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public function finish(): int
|
||||||
|
{
|
||||||
|
$centralDirectoryStartOffsetOnDisk = $this->offset;
|
||||||
|
$sizeOfCentralDirectory = 0;
|
||||||
|
|
||||||
|
// add trailing cdr file records
|
||||||
|
foreach ($this->centralDirectoryRecords as $centralDirectoryRecord) {
|
||||||
|
$this->send($centralDirectoryRecord);
|
||||||
|
$sizeOfCentralDirectory += strlen($centralDirectoryRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 64bit headers (if applicable)
|
||||||
|
if (count($this->centralDirectoryRecords) >= 0xFFFF ||
|
||||||
|
$centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF ||
|
||||||
|
$sizeOfCentralDirectory > 0xFFFFFFFF) {
|
||||||
|
if (!$this->enableZip64) {
|
||||||
|
throw new OverflowException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->send(Zip64\EndOfCentralDirectory::generate(
|
||||||
|
versionMadeBy: self::ZIP_VERSION_MADE_BY,
|
||||||
|
versionNeededToExtract: Version::ZIP64->value,
|
||||||
|
numberOfThisDisk: 0,
|
||||||
|
numberOfTheDiskWithCentralDirectoryStart: 0,
|
||||||
|
numberOfCentralDirectoryEntriesOnThisDisk: count($this->centralDirectoryRecords),
|
||||||
|
numberOfCentralDirectoryEntries: count($this->centralDirectoryRecords),
|
||||||
|
sizeOfCentralDirectory: $sizeOfCentralDirectory,
|
||||||
|
centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk,
|
||||||
|
extensibleDataSector: '',
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->send(Zip64\EndOfCentralDirectoryLocator::generate(
|
||||||
|
numberOfTheDiskWithZip64CentralDirectoryStart: 0x00,
|
||||||
|
zip64centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk + $sizeOfCentralDirectory,
|
||||||
|
totalNumberOfDisks: 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add trailing cdr eof record
|
||||||
|
$numberOfCentralDirectoryEntries = min(count($this->centralDirectoryRecords), 0xFFFF);
|
||||||
|
$this->send(EndOfCentralDirectory::generate(
|
||||||
|
numberOfThisDisk: 0x00,
|
||||||
|
numberOfTheDiskWithCentralDirectoryStart: 0x00,
|
||||||
|
numberOfCentralDirectoryEntriesOnThisDisk: $numberOfCentralDirectoryEntries,
|
||||||
|
numberOfCentralDirectoryEntries: $numberOfCentralDirectoryEntries,
|
||||||
|
sizeOfCentralDirectory: min($sizeOfCentralDirectory, 0xFFFFFFFF),
|
||||||
|
centralDirectoryStartOffsetOnDisk: min($centralDirectoryStartOffsetOnDisk, 0xFFFFFFFF),
|
||||||
|
zipFileComment: $this->comment,
|
||||||
|
));
|
||||||
|
|
||||||
|
$size = $this->offset;
|
||||||
|
|
||||||
|
// The End
|
||||||
|
$this->clear();
|
||||||
|
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StreamInterface|resource|null $outputStream
|
||||||
|
* @return resource
|
||||||
|
*/
|
||||||
|
private static function normalizeStream($outputStream)
|
||||||
|
{
|
||||||
|
if ($outputStream instanceof StreamInterface) {
|
||||||
|
return StreamWrapper::getResource($outputStream);
|
||||||
|
}
|
||||||
|
if (is_resource($outputStream)) {
|
||||||
|
return $outputStream;
|
||||||
|
}
|
||||||
|
$resource = fopen('php://output', 'wb');
|
||||||
|
|
||||||
|
if ($resource === false) {
|
||||||
|
throw new RuntimeException('fopen of php://output failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record sent bytes
|
||||||
|
*/
|
||||||
|
private function recordSentBytes(int $sentBytes): void
|
||||||
|
{
|
||||||
|
$this->offset += $sentBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send string, sending HTTP headers if necessary.
|
||||||
|
* Flush output after write if configure option is set.
|
||||||
|
*/
|
||||||
|
private function send(string $data): void
|
||||||
|
{
|
||||||
|
if (!$this->ready) {
|
||||||
|
throw new RuntimeException('Archive is already finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->operationMode === OperationMode::NORMAL && $this->sendHttpHeaders) {
|
||||||
|
$this->sendHttpHeaders();
|
||||||
|
$this->sendHttpHeaders = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->recordSentBytes(strlen($data));
|
||||||
|
|
||||||
|
if ($this->operationMode === OperationMode::NORMAL) {
|
||||||
|
if (fwrite($this->outputStream, $data) === false) {
|
||||||
|
throw new ResourceActionException('fwrite', $this->outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->flushOutput) {
|
||||||
|
// flush output buffer if it is on and flushable
|
||||||
|
$status = ob_get_status();
|
||||||
|
if (isset($status['flags']) && is_int($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) {
|
||||||
|
ob_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush system buffers after flushing userspace output buffer
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send HTTP headers for this stream.
|
||||||
|
*/
|
||||||
|
private function sendHttpHeaders(): void
|
||||||
|
{
|
||||||
|
// grab content disposition
|
||||||
|
$disposition = $this->contentDisposition;
|
||||||
|
|
||||||
|
if ($this->outputName !== null) {
|
||||||
|
// Various different browsers dislike various characters here. Strip them all for safety.
|
||||||
|
$safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName));
|
||||||
|
|
||||||
|
// Check if we need to UTF-8 encode the filename
|
||||||
|
$urlencoded = rawurlencode($safeOutput);
|
||||||
|
$disposition .= "; filename*=UTF-8''{$urlencoded}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => $this->contentType,
|
||||||
|
'Content-Disposition' => $disposition,
|
||||||
|
'Pragma' => 'public',
|
||||||
|
'Cache-Control' => 'public, must-revalidate',
|
||||||
|
'Content-Transfer-Encoding' => 'binary',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $key => $val) {
|
||||||
|
($this->httpHeaderCallback)("$key: $val");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all internal variables. Note that the stream object is not
|
||||||
|
* usable after this.
|
||||||
|
*/
|
||||||
|
private function clear(): void
|
||||||
|
{
|
||||||
|
$this->centralDirectoryRecords = [];
|
||||||
|
$this->offset = 0;
|
||||||
|
|
||||||
|
if ($this->operationMode === OperationMode::NORMAL) {
|
||||||
|
$this->ready = false;
|
||||||
|
$this->recordedSimulation = [];
|
||||||
|
} else {
|
||||||
|
$this->operationMode = OperationMode::NORMAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ZipStream\Test;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipArchive;
|
||||||
|
use ZipStream\Stream\CallbackStreamWrapper;
|
||||||
|
use ZipStream\ZipStream;
|
||||||
|
|
||||||
|
final class CallbackOutputTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
// Clean up any registered callbacks to prevent memory leaks in tests
|
||||||
|
CallbackStreamWrapper::cleanup();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDataIsForwardedToCallback(): void
|
||||||
|
{
|
||||||
|
$buf = '';
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$buf): void { $buf .= $chunk; }
|
||||||
|
),
|
||||||
|
sendHttpHeaders: false
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('hello.txt', 'Hello World');
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
$tmp = tmpfile();
|
||||||
|
fwrite($tmp, $buf);
|
||||||
|
rewind($tmp);
|
||||||
|
|
||||||
|
$meta = stream_get_meta_data($tmp);
|
||||||
|
$za = new ZipArchive();
|
||||||
|
$za->open($meta['uri']);
|
||||||
|
|
||||||
|
$content = $za->getFromName('hello.txt');
|
||||||
|
$za->close();
|
||||||
|
fclose($tmp);
|
||||||
|
|
||||||
|
$this->assertSame('Hello World', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMultipleSimultaneousStreams(): void
|
||||||
|
{
|
||||||
|
$buf1 = '';
|
||||||
|
$buf2 = '';
|
||||||
|
|
||||||
|
$stream1 = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$buf1): void { $buf1 .= $chunk; }
|
||||||
|
);
|
||||||
|
$stream2 = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$buf2): void { $buf2 .= $chunk; }
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertIsResource($stream1);
|
||||||
|
$this->assertIsResource($stream2);
|
||||||
|
|
||||||
|
fwrite($stream1, 'data1');
|
||||||
|
fwrite($stream2, 'data2');
|
||||||
|
fclose($stream1);
|
||||||
|
fclose($stream2);
|
||||||
|
|
||||||
|
$this->assertSame('data1', $buf1);
|
||||||
|
$this->assertSame('data2', $buf2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExceptionHandlingInCallback(): void
|
||||||
|
{
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk): void {
|
||||||
|
throw new RuntimeException('Callback error');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Callback function failed during stream write: Callback error');
|
||||||
|
|
||||||
|
fwrite($stream, 'test data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLargeDataChunks(): void
|
||||||
|
{
|
||||||
|
$receivedChunks = [];
|
||||||
|
$totalBytes = 0;
|
||||||
|
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$receivedChunks, &$totalBytes): void {
|
||||||
|
$receivedChunks[] = strlen($chunk);
|
||||||
|
$totalBytes += strlen($chunk);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write large chunks of data
|
||||||
|
$largeData = str_repeat('x', 65536); // 64KB
|
||||||
|
fwrite($stream, $largeData);
|
||||||
|
fwrite($stream, $largeData);
|
||||||
|
fclose($stream);
|
||||||
|
|
||||||
|
$this->assertSame(131072, $totalBytes); // 128KB total
|
||||||
|
$this->assertNotEmpty($receivedChunks);
|
||||||
|
// Large data should be written (possibly in multiple chunks)
|
||||||
|
$this->assertGreaterThan(0, max($receivedChunks));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStreamPositionTracking(): void
|
||||||
|
{
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk): void { /* no-op */ }
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(0, ftell($stream));
|
||||||
|
|
||||||
|
fwrite($stream, 'hello');
|
||||||
|
$this->assertSame(5, ftell($stream));
|
||||||
|
|
||||||
|
fwrite($stream, ' world');
|
||||||
|
$this->assertSame(11, ftell($stream));
|
||||||
|
|
||||||
|
fclose($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidModeRejection(): void
|
||||||
|
{
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk): void { /* no-op */ }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close the stream first
|
||||||
|
fclose($stream);
|
||||||
|
|
||||||
|
// Try to open with read mode - should fail
|
||||||
|
$readStream = fopen('zipcb://invalid', 'rb');
|
||||||
|
$this->assertFalse($readStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStreamStatistics(): void
|
||||||
|
{
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk): void { /* no-op */ }
|
||||||
|
);
|
||||||
|
|
||||||
|
fwrite($stream, 'test data');
|
||||||
|
|
||||||
|
$stats = fstat($stream);
|
||||||
|
$this->assertIsArray($stats);
|
||||||
|
$this->assertSame(9, $stats['size']); // Length of 'test data'
|
||||||
|
$this->assertSame(0o100666, $stats['mode']); // Regular file permissions
|
||||||
|
|
||||||
|
fclose($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProgressTracking(): void
|
||||||
|
{
|
||||||
|
$progress = [];
|
||||||
|
$totalBytes = 0;
|
||||||
|
|
||||||
|
$zip = new ZipStream(
|
||||||
|
outputStream: CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$progress, &$totalBytes): void {
|
||||||
|
$totalBytes += strlen($chunk);
|
||||||
|
$progress[] = $totalBytes;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
sendHttpHeaders: false
|
||||||
|
);
|
||||||
|
|
||||||
|
$zip->addFile('file1.txt', 'Content 1');
|
||||||
|
$zip->addFile('file2.txt', 'Content 2');
|
||||||
|
$zip->finish();
|
||||||
|
|
||||||
|
$this->assertNotEmpty($progress);
|
||||||
|
$this->assertGreaterThan(0, $totalBytes);
|
||||||
|
$this->assertTrue(count($progress) > 1, 'Should have multiple progress updates');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCallbackCleanupOnClose(): void
|
||||||
|
{
|
||||||
|
$callbackExecuted = false;
|
||||||
|
|
||||||
|
$stream = CallbackStreamWrapper::open(
|
||||||
|
static function (string $chunk) use (&$callbackExecuted): void {
|
||||||
|
$callbackExecuted = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fwrite($stream, 'test');
|
||||||
|
$this->assertTrue($callbackExecuted);
|
||||||
|
|
||||||
|
fclose($stream);
|
||||||
|
|
||||||
|
// After closing, callback should be cleaned up
|
||||||
|
// We can't directly test this, but the tearDown cleanup should work without issues
|
||||||
|
$this->assertTrue(true); // Placeholder assertion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?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 : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user