diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 107aa2530..a70a01cba 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -11,6 +11,7 @@ on: env: COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" COMPOSER_UPDATE_FLAGS: "" + COMPOSER_TESTS_ARE_RUNNING: "1" SYMFONY_PHPUNIT_VERSION: "8.3" SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1" @@ -108,7 +109,7 @@ jobs: run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" - name: "Cache dependencies installed with composer" - uses: "actions/cache@v1" + uses: "actions/cache@v2" with: path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" key: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}-${{ hashFiles('**/composer.lock') }}" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index d2894e09b..d92872609 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -34,14 +34,13 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1" php-version: "${{ matrix.php-version }}" - tools: "cs2pr" - name: "Determine composer cache directory" id: "determine-composer-cache-directory" run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" - name: "Cache dependencies installed with composer" - uses: "actions/cache@v1" + uses: "actions/cache@v2" with: path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" key: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}-${{ hashFiles('**/composer.lock') }}" @@ -52,5 +51,5 @@ jobs: - name: Run PHPStan run: | - bin/composer require --dev phpstan/phpstan:^0.12.26 phpunit/phpunit:^7.5 --with-all-dependencies - vendor/bin/phpstan analyse --configuration=phpstan/config.neon || vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr + bin/composer require --dev phpstan/phpstan:^0.12.37 phpunit/phpunit:^7.5.20 --with-all-dependencies + vendor/bin/phpstan analyse --configuration=phpstan/config.neon diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce4da235..f52a54ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### [2.0.0-alpha3] 2020-08-03 + + * Breaking: Zip archives loaded by artifact repositories must now have a composer.json on top level, or a max of one folder on top level of the archive + * Added --no-dev support to `show` and `outdated` commands to skip dev requirements + * Added support for multiple --repository flags being passed into the `create-project` command, only useful in combination with `--add-repository` to persist them to composer.json + * Added a new optional `list` API endpoint for v2-format composer repositories, see [UPGRADE](UPGRADE-2.0.md) for details + * Fixed `show -a` command not listing anything + * Fixed solver bug where it ended in a "Reached invalid decision id 0" + * Fixed updates of git-installed packages on windows + * Lots of minor bug fixes + ### [2.0.0-alpha2] 2020-06-24 * Added parallel installation of packages (requires OSX/Linux/WSL, and that `unzip` is present in PATH) @@ -46,6 +57,12 @@ * Fixed suggest output being very spammy, it now is only one line long and shows more rarely * Fixed conflict rules like e.g. >=5 from matching dev-master, as it is not normalized to 9999999-dev internally anymore +### [1.10.10] 2020-08-03 + + * Fixed `create-project` not triggering events while installing the root package + * Fixed PHP 8 compatibility issue + * Fixed `self-update` to avoid automatically upgrading to the next major version once it becomes stable + ### [1.10.9] 2020-07-16 * Fixed Bitbucket redirect loop when credentials are outdated @@ -921,8 +938,10 @@ * Initial release +[2.0.0-alpha3]: https://github.com/composer/composer/compare/2.0.0-alpha2...2.0.0-alpha3 [2.0.0-alpha2]: https://github.com/composer/composer/compare/2.0.0-alpha1...2.0.0-alpha2 [2.0.0-alpha1]: https://github.com/composer/composer/compare/1.10.7...2.0.0-alpha1 +[1.10.10]: https://github.com/composer/composer/compare/1.10.9...1.10.10 [1.10.9]: https://github.com/composer/composer/compare/1.10.8...1.10.9 [1.10.8]: https://github.com/composer/composer/compare/1.10.7...1.10.8 [1.10.7]: https://github.com/composer/composer/compare/1.10.6...1.10.7 diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index df2b41359..a2da9c2e2 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -80,4 +80,6 @@ The providers-api is optional, but if you implement it it should return packages This is also optional, it should accept an optional `?filter=xx` query param, which can contain `*` as wildcards matching any substring. -It must return an array of package names as `{"packageNames": ["a/b", "c/d"]}`. See https://packagist.org/packages/list.json?filter=composer/* for example. +It must return an array of package names as `{"packageNames": ["a/b", "c/d"]}`. See for example. + +It should return the names of package which names match the filter (or all names if no filter is present). Replace/provide rules should not be considered here. diff --git a/composer.lock b/composer.lock index 73c009d82..86f89f991 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.2.7", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" + "reference": "8a7ecad675253e4654ea05505233285377405215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", "shasum": "" }, "require": { @@ -63,19 +63,23 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.7" + "source": "https://github.com/composer/ca-bundle/tree/1.2.8" }, "funding": [ { "url": "https://packagist.com", "type": "custom" }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2020-04-08T08:27:21+00:00" + "time": "2020-08-23T12:54:47+00:00" }, { "name": "composer/semver", @@ -239,16 +243,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", "shasum": "" }, "require": { @@ -282,7 +286,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.2" + "source": "https://github.com/composer/xdebug-handler/tree/1.4.3" }, "funding": [ { @@ -298,7 +302,7 @@ "type": "tidelift" } ], - "time": "2020-06-04T11:16:35+00:00" + "time": "2020-08-19T10:27:58+00:00" }, { "name": "justinrainbow/json-schema", @@ -470,16 +474,16 @@ }, { "name": "seld/jsonlint", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" + "reference": "3d5eb71705adfa34bd34b993400622932b2f62fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/3d5eb71705adfa34bd34b993400622932b2f62fd", + "reference": "3d5eb71705adfa34bd34b993400622932b2f62fd", "shasum": "" }, "require": { @@ -529,7 +533,7 @@ "type": "tidelift" } ], - "time": "2020-04-30T19:05:18+00:00" + "time": "2020-08-13T09:07:59+00:00" }, { "name": "seld/phar-utils", @@ -810,7 +814,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -889,7 +893,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -949,7 +953,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/master" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.18.1" }, "funding": [ { diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 5dd1726bd..45e1cc727 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -142,6 +142,9 @@ reinstalling the project you can feel confident the dependencies installed are still working even if your dependencies released many new versions since then. (See note below about using the `update` command.) +> **Note:** For libraries it is not necessary to commit the lock +> file, see also: [Libraries - Lock file](02-libraries.md#lock-file). + ## Updating dependencies to their latest versions As mentioned above, the `composer.lock` file prevents you from automatically getting @@ -165,9 +168,6 @@ If you only want to install, upgrade or remove one dependency, you can explicitl php composer.phar update monolog/monolog [...] ``` -> **Note:** For libraries it is not necessary to commit the lock -> file, see also: [Libraries - Lock file](02-libraries.md#lock-file). - ## Packagist [Packagist](https://packagist.org/) is the main Composer repository. A Composer diff --git a/doc/05-repositories.md b/doc/05-repositories.md index b32d92690..12a9dced0 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -516,7 +516,7 @@ There are some cases, when there is no ability to have one of the previously mentioned repository types online, even the VCS one. Typical example could be cross-organisation library exchange through built artifacts. Of course, most of the times they are private. To simplify maintenance, one can simply use a -repository of type `artifact` with a folder containing ZIP archives of those +repository of type `artifact` with a folder containing ZIP or TAR archives of those private packages: ```json diff --git a/doc/06-config.md b/doc/06-config.md index b7e5e8e45..ccbdb3b07 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -85,11 +85,11 @@ gitlab.com the domain names must be also specified with the ## gitlab-token -A list of domain names and private tokens. Private token can be either simple -string, or array with username and token. For example using `{"gitlab.com": +A list of domain names and private tokens. Private token can be either simple +string, or array with username and token. For example using `{"gitlab.com": "privatetoken"}` as the value of this option will use `privatetoken` to access private repositories on gitlab. Using `{"gitlab.com": {"username": "gitlabuser", - "token": "privatetoken"}}` will use both username and token for gitlab deploy + "token": "privatetoken"}}` will use both username and token for gitlab deploy token functionality (https://docs.gitlab.com/ee/user/project/deploy_tokens/) Please note: If the package is not hosted at gitlab.com the domain names must be also specified with the @@ -204,6 +204,10 @@ downloads. When the garbage collection is periodically ran, this is the maximum size the cache will be able to use. Older (less used) files will be removed first until the cache fits. +## cache-read-only + +Defaults to `false`. Whether to use the Composer cache in read-only mode. + ## bin-compat Defaults to `auto`. Determines the compatibility of the binaries to be installed. diff --git a/res/composer-schema.json b/res/composer-schema.json index 04db6d3a3..8aa401580 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -237,6 +237,10 @@ "type": ["string", "integer"], "description": "The cache max size for the files cache, defaults to \"300MiB\"." }, + "cache-read-only": { + "type": ["boolean"], + "description": "Whether to use the Composer cache in read-only mode." + }, "bin-compat": { "enum": ["auto", "full"], "description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed) and can be \"full\" (compatible with both Windows and Unix-based systems)." diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 2a4e7756f..97be1e1a2 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -30,19 +30,22 @@ class Cache private $enabled = true; private $allowlist; private $filesystem; + private $readOnly; /** * @param IOInterface $io * @param string $cacheDir location of the cache * @param string $allowlist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance + * @param bool $readOnly whether the cache is in readOnly mode */ - public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null, $readOnly = false) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); + $this->readOnly = (bool) $readOnly; if (!self::isUsable($cacheDir)) { $this->enabled = false; @@ -59,6 +62,22 @@ class Cache } } + /** + * @param bool $readOnly + */ + public function setReadOnly($readOnly) + { + $this->readOnly = (bool) $readOnly; + } + + /** + * @return bool + */ + public function isReadOnly() + { + return $this->readOnly; + } + public static function isUsable($path) { return !preg_match('{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $path); @@ -90,7 +109,7 @@ class Cache public function write($file, $contents) { - if ($this->enabled) { + if ($this->enabled && !$this->readOnly) { $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -128,7 +147,7 @@ class Cache */ public function copyFrom($file, $source) { - if ($this->enabled) { + if ($this->enabled && !$this->readOnly) { $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php index 2f511641e..bdbdd80cf 100644 --- a/src/Composer/Command/ClearCacheCommand.php +++ b/src/Composer/Command/ClearCacheCommand.php @@ -59,6 +59,7 @@ EOT continue; } $cache = new Cache($io, $cachePath); + $cache->setReadOnly($config->get('cache-read-only')); if (!$cache->isEnabled()) { $io->writeError("Cache is not enabled ($key): $cachePath"); diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index d79d22df2..fab414deb 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -135,8 +135,36 @@ EOT $latest = $versionsUtil->getLatest(); $latestStable = $versionsUtil->getLatest('stable'); + try { + $latestPreview = $versionsUtil->getLatest('preview'); + } catch (\UnexpectedValueException $e) { + $latestPreview = $latestStable; + } $latestVersion = $latest['version']; $updateVersion = $input->getArgument('version') ?: $latestVersion; + $currentMajorVersion = preg_replace('{^(\d+).*}', '$1', Composer::getVersion()); + $updateMajorVersion = preg_replace('{^(\d+).*}', '$1', $updateVersion); + $previewMajorVersion = preg_replace('{^(\d+).*}', '$1', $latestPreview['version']); + + if ($versionsUtil->getChannel() === 'stable' && !$input->getArgument('version')) { + // if requesting stable channel and no specific version, avoid automatically upgrading to the next major + // simply output a warning that the next major stable is available and let users upgrade to it manually + if ($currentMajorVersion < $updateMajorVersion) { + $skippedVersion = $updateVersion; + + $versionsUtil->setChannel($currentMajorVersion); + + $latest = $versionsUtil->getLatest(); + $latestStable = $versionsUtil->getLatest('stable'); + $latestVersion = $latest['version']; + $updateVersion = $latestVersion; + + $io->writeError('A new stable major version of Composer is available ('.$skippedVersion.'), run "composer self-update --'.$updateMajorVersion.'" to update to it. See also https://github.com/composer/composer/releases for changelogs.'); + } elseif ($currentMajorVersion < $previewMajorVersion) { + // promote next major version if available in preview + $io->writeError('A preview release of the next major version of Composer is available ('.$latestPreview['version'].'), run "composer self-update --preview" to give it a try. See also https://github.com/composer/composer/releases for changelogs.'); + } + } if ($requestedChannel && is_numeric($requestedChannel) && substr($latestStable['version'], 0, 1) !== $requestedChannel) { $io->writeError('Warning: You forced the install of '.$latestVersion.' via --'.$requestedChannel.', but '.$latestStable['version'].' is the latest stable version. Updating to it via composer self-update --stable is recommended.'); @@ -243,7 +271,12 @@ TAGSPUBKEY $signature = json_decode($signature, true); $signature = base64_decode($signature['sha384']); $verified = 1 === openssl_verify(file_get_contents($tempFilename), $signature, $pubkeyid, $algo); - openssl_free_key($pubkeyid); + + // PHP 8 automatically frees the key instance and deprecates the function + if (PHP_VERSION_ID < 80000) { + openssl_free_key($pubkeyid); + } + if (!$verified) { throw new \RuntimeException('The phar signature did not match the file you downloaded, this means your public keys are outdated or that the phar file is corrupt/has been modified'); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 5541b53a8..635d56bf3 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -578,8 +578,8 @@ EOT $matchedPackage = null; $versions = array(); - $matches = $repositorySet->findPackages($name, $constraint); $pool = $repositorySet->createPoolForPackage($name); + $matches = $pool->whatProvides($name, $constraint); foreach ($matches as $index => $package) { // select an exact match if it is in the installed repo and no specific version was required if (null === $version && $installedRepo->hasPackage($package)) { diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 455ae6db7..54d2e360c 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -41,6 +41,7 @@ class Config 'cache-ttl' => 15552000, // 6 months 'cache-files-ttl' => null, // fallback to cache-ttl 'cache-files-maxsize' => '300MiB', + 'cache-read-only' => false, 'bin-compat' => 'auto', 'discard-changes' => false, 'autoloader-suffix' => null, @@ -211,6 +212,7 @@ class Config public function get($key, $flags = 0) { switch ($key) { + // strings/paths with env var and {$refs} support case 'vendor-dir': case 'bin-dir': case 'process-timeout': @@ -234,20 +236,34 @@ class Config return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val); + // booleans with env var support + case 'cache-read-only': case 'htaccess-protect': - $value = $this->getComposerEnv('COMPOSER_HTACCESS_PROTECT'); - if (false === $value) { - $value = $this->config[$key]; - } - return $value !== 'false' && (bool) $value; + // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config + $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); + $val = $this->getComposerEnv($env); + if (false === $val) { + $val = $this->config[$key]; + } + return $val !== 'false' && (bool) $val; + + // booleans without env var support + case 'disable-tls': + case 'secure-http': + case 'use-github-api': + case 'lock': + return $this->config[$key] !== 'false' && (bool) $this->config[$key]; + + // ints without env var support case 'cache-ttl': return (int) $this->config[$key]; + // numbers with kb/mb/gb support, without env var support case 'cache-files-maxsize': if (!preg_match('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $this->config[$key], $matches)) { throw new \RuntimeException( - "Could not parse the value of 'cache-files-maxsize': {$this->config[$key]}" + "Could not parse the value of '$key': {$this->config[$key]}" ); } $size = $matches[1]; @@ -269,6 +285,7 @@ class Config return $size; + // special cases below case 'cache-files-ttl': if (isset($this->config[$key])) { return (int) $this->config[$key]; @@ -326,14 +343,6 @@ class Config return $protos; - case 'disable-tls': - return $this->config[$key] !== 'false' && (bool) $this->config[$key]; - case 'secure-http': - return $this->config[$key] !== 'false' && (bool) $this->config[$key]; - case 'use-github-api': - return $this->config[$key] !== 'false' && (bool) $this->config[$key]; - case 'lock': - return $this->config[$key] !== 'false' && (bool) $this->config[$key]; default: if (!isset($this->config[$key])) { return null; diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 622938476..acd0e4c0a 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -22,6 +22,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Seld\JsonLint\ParsingException; use Composer\Command; use Composer\Composer; use Composer\Factory; @@ -191,6 +192,20 @@ class Application extends BaseApplication } } catch (NoSslException $e) { // suppress these as they are not relevant at this point + } catch (ParsingException $e) { + $details = $e->getDetails(); + + $file = realpath(Factory::getComposerFile()); + + $line = null; + if ($details && isset($details['line'])) { + $line = $details['line']; + } + + $ghe = new GithubActionError($this->io); + $ghe->emit($e->getMessage(), $file, $line); + + throw $e; } $this->hasPluginCommands = true; @@ -237,7 +252,7 @@ class Application extends BaseApplication if (function_exists('posix_getuid') && posix_getuid() === 0) { if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { $io->writeError('Do not run Composer as root/super user! See https://getcomposer.org/root for details'); - + if ($io->isInteractive()) { if (!$io->askConfirmation('Continue as root/super user [yes]? ', true)) { return 1; @@ -307,8 +322,13 @@ class Application extends BaseApplication } catch (ScriptExecutionException $e) { return (int) $e->getCode(); } catch (\Exception $e) { + $ghe = new GithubActionError($this->io); + $ghe->emit($e->getMessage()); + $this->hintCommonErrors($e); + restore_error_handler(); + throw $e; } } diff --git a/src/Composer/Console/GithubActionError.php b/src/Composer/Console/GithubActionError.php new file mode 100644 index 000000000..bbf5eaca1 --- /dev/null +++ b/src/Composer/Console/GithubActionError.php @@ -0,0 +1,40 @@ +io = $io; + } + + /** + * @param string $message + * @param null|string $file + * @param null|int $line + */ + public function emit($message, $file = null, $line = null) + { + if (getenv('GITHUB_ACTIONS') && !getenv('COMPOSER_TESTS_ARE_RUNNING')) { + // newlines need to be encoded + // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $message = str_replace("\n", '%0A', $message); + + if ($file && $line) { + $this->io->write("::error file=". $file .",line=". $line ."::". $message); + } elseif ($file) { + $this->io->write("::error file=". $file ."::". $message); + } else { + $this->io->write("::error ::". $message); + } + } + } +} diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 235987aa3..4f3bdefa3 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -60,7 +60,7 @@ class DefaultPolicy implements PolicyInterface $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } - $selected = \call_user_func_array('array_merge', $packages); + $selected = \call_user_func_array('array_merge', array_values($packages)); // now sort the result across all packages to respect replaces across packages usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 6d94b068b..9d4405330 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -276,27 +276,19 @@ class Problem // check if the package is found when bypassing stability checks if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + // we must first verify if a valid package would be found in a lower priority repository + if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + return self::computeCheckForLowerPrioRepo($isVerbose, $packageName, $constraint, $packages, $allReposPackages, 'minimum-stability'); + } + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); } - // check if the package is found when bypassing the constraint check - if ($packages = $repositorySet->findPackages($packageName, null)) { + // check if the package is found when bypassing the constraint and stability checks + if ($packages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { // we must first verify if a valid package would be found in a lower priority repository if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { - $higherRepoPackages = $repositorySet->findPackages($packageName, null); - $nextRepoPackages = array(); - $nextRepo = null; - - foreach ($allReposPackages as $package) { - if ($nextRepo === null || $nextRepo === $package->getRepository()) { - $nextRepoPackages[] = $package; - $nextRepo = $package->getRepository(); - } else { - break; - } - } - - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); + return self::computeCheckForLowerPrioRepo($isVerbose, $packageName, $constraint, $packages, $allReposPackages, 'constraint'); } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.'); @@ -396,6 +388,24 @@ class Problem return false; } + private static function computeCheckForLowerPrioRepo($isVerbose, $packageName, $constraint, array $higherRepoPackages, array $allReposPackages, $reason) + { + $nextRepoPackages = array(); + $nextRepo = null; + + foreach ($allReposPackages as $package) { + if ($nextRepo === null || $nextRepo === $package->getRepository()) { + $nextRepoPackages[] = $package; + $nextRepo = $package->getRepository(); + } else { + break; + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your '.$reason.' and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); + + } + /** * Turns a constraint into text usable in a sentence describing a request * diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 7cf19deee..ba19aa58b 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -46,10 +46,17 @@ abstract class ArchiveDownloader extends FileDownloader $this->io->writeError('Extracting archive', false); } - $this->filesystem->emptyDirectory($path); + $vendorDir = $this->config->get('vendor-dir'); + + // clean up the target directory, unless it contains the vendor dir, as the vendor dir contains + // the archive to be extracted. This is the case when installing with create-project in the current directory + // but in that case we ensure the directory is empty already in ProjectInstaller so no need to empty it here. + if (false === strpos($this->filesystem->normalizePath($vendorDir), $this->filesystem->normalizePath($path).DIRECTORY_SEPARATOR)) { + $this->filesystem->emptyDirectory($path); + } do { - $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + $temporaryDir = $vendorDir.'/composer/'.substr(md5(uniqid('', true)), 0, 8); } while (is_dir($temporaryDir)); $this->addCleanupPath($package, $temporaryDir); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 8fbb48e9c..9fa498af8 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -187,7 +187,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $url = reset($urls); $cacheKey = $url['cacheKey']; - if ($cache) { + if ($cache && !$cache->isReadOnly()) { $self->lastCacheWrites[$package->getName()] = $cacheKey; $cache->copyFrom($cacheKey, $fileName); } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 177644735..f15539a87 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -146,10 +146,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface if (!empty($this->cachedPackages[$target->getId()][$ref])) { $msg = "Checking out ".$this->getShortHash($ref).' from cache'; - $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; + $command = '(git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer)) && git remote set-url composer %sanitizedUrl%'; } else { $msg = "Checking out ".$this->getShortHash($ref); - $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; + $command = '(git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)) && git remote set-url composer %sanitizedUrl%'; if (getenv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); } diff --git a/src/Composer/Downloader/MaxFileSizeExceededException.php b/src/Composer/Downloader/MaxFileSizeExceededException.php new file mode 100644 index 000000000..f50b52e79 --- /dev/null +++ b/src/Composer/Downloader/MaxFileSizeExceededException.php @@ -0,0 +1,7 @@ +getDistUrl()); - if ($path === $realUrl) { + if (realpath($path) === $realUrl) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 7fd9af344..218a53457 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -477,6 +477,7 @@ class Factory $cache = null; if ($config->get('cache-files-ttl') > 0) { $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); + $cache->setReadOnly($config->get('cache-read-only')); } $fs = new Filesystem($process); diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index c2c6dbd3e..7bf2c0686 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -46,7 +46,7 @@ class InstalledVersions * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName - * @param ?string $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @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 */ diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 40a041927..523ecca6b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -13,6 +13,7 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; +use Composer\Console\GithubActionError; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\LocalRepoTransaction; use Composer\DependencyResolver\LockTransaction; @@ -387,9 +388,13 @@ class Installer $this->io->writeError('Updating dependencies'); // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata - if ($this->updateMirrors) { + if ($this->updateMirrors && $lockedRepository) { foreach ($lockedRepository->getPackages() as $lockedPackage) { - $request->requireName($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + // exclude alias packages here as for root aliases, both alias and aliased are + // present in the lock repo and we only want to require the aliased version + if (!$lockedPackage instanceof AliasPackage) { + $request->requireName($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + } } } else { $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); @@ -412,12 +417,18 @@ class Installer $ruleSetSize = $solver->getRuleSetSize(); $solver = null; } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); + $err = 'Your requirements could not be resolved to an installable set of packages.'; + $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose()); + + $this->io->writeError(''. $err .'', true, IOInterface::QUIET); + $this->io->writeError($prettyProblem); if (!$this->devMode) { $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); } + $ghe = new GithubActionError($this->io); + $ghe->emit($err."\n".$prettyProblem); + return max(1, $e->getCode()); } @@ -571,10 +582,16 @@ class Installer $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $solver = null; } catch (SolverProblemsException $e) { - $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); + $err = 'Unable to find a compatible set of packages based on your non-dev requirements alone.'; + $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose(), true); + + $this->io->writeError(''. $err .'', true, IOInterface::QUIET); $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose(), true)); + $this->io->writeError($prettyProblem); + + $ghe = new GithubActionError($this->io); + $ghe->emit($err."\n".$prettyProblem); return max(1, $e->getCode()); } @@ -637,8 +654,14 @@ class Installer return 1; } } catch (SolverProblemsException $e) { - $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); + $err = 'Your lock file does not contain a compatible set of packages. Please run composer update.'; + $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose()); + + $this->io->writeError(''. $err .'', true, IOInterface::QUIET); + $this->io->writeError($prettyProblem); + + $ghe = new GithubActionError($this->io); + $ghe->emit($err."\n".$prettyProblem); return max(1, $e->getCode()); } diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index aff73be1d..2c6621f0c 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -111,25 +111,27 @@ class ValidatingArrayLoader implements LoaderInterface if (is_array($this->config['license']) || is_string($this->config['license'])) { $licenses = (array) $this->config['license']; - // strip proprietary since it's not a valid SPDX identifier, but is accepted by composer - foreach ($licenses as $key => $license) { - if ('proprietary' === $license) { - unset($licenses[$key]); - } - } - $licenseValidator = new SpdxLicenses(); - if (count($licenses) === 1 && !$licenseValidator->validate($licenses) && $licenseValidator->validate(trim($licenses[0]))) { - $this->warnings[] = sprintf( - 'License %s must not contain extra spaces, make sure to trim it.', - json_encode($this->config['license']) - ); - } elseif (array() !== $licenses && !$licenseValidator->validate($licenses)) { - $this->warnings[] = sprintf( - 'License %s is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.' . PHP_EOL . - 'If the software is closed-source, you may use "proprietary" as license.', - json_encode($this->config['license']) - ); + foreach ($licenses as $license) { + // replace proprietary by MIT for validation purposes since it's not a valid SPDX identifier, but is accepted by composer + if ('proprietary' === $license) { + continue; + } + $licenseToValidate = str_replace('proprietary', 'MIT', $license); + if (!$licenseValidator->validate($licenseToValidate)) { + if ($licenseValidator->validate(trim($licenseToValidate))) { + $this->warnings[] = sprintf( + 'License %s must not contain extra spaces, make sure to trim it.', + json_encode($license) + ); + } else { + $this->warnings[] = sprintf( + 'License %s is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.' . PHP_EOL . + 'If the software is closed-source, you may use "proprietary" as license.', + json_encode($license) + ); + } + } } } } diff --git a/src/Composer/Platform/HhvmDetector.php b/src/Composer/Platform/HhvmDetector.php new file mode 100644 index 000000000..ac63d047b --- /dev/null +++ b/src/Composer/Platform/HhvmDetector.php @@ -0,0 +1,61 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Platform; + +use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; +use Symfony\Component\Process\ExecutableFinder; + +class HhvmDetector +{ + private static $hhvmVersion; + private $executableFinder; + private $processExecutor; + + public function __construct(ExecutableFinder $executableFinder = null, ProcessExecutor $processExecutor = null) + { + $this->executableFinder = $executableFinder; + $this->processExecutor = $processExecutor; + } + + public function reset() + { + self::$hhvmVersion = null; + } + + public function getVersion() + { + if (null !== self::$hhvmVersion) { + return self::$hhvmVersion ?: null; + } + + self::$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; + if (self::$hhvmVersion === null && !Platform::isWindows()) { + self::$hhvmVersion = false; + $this->executableFinder = $this->executableFinder ?: new ExecutableFinder(); + $hhvmPath = $this->executableFinder->find('hhvm'); + if ($hhvmPath !== null) { + $this->processExecutor = $this->processExecutor ?: new ProcessExecutor(); + $exitCode = $this->processExecutor->execute( + ProcessExecutor::escape($hhvmPath). + ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', + self::$hhvmVersion + ); + if ($exitCode !== 0) { + self::$hhvmVersion = false; + } + } + } + + return self::$hhvmVersion; + } +} diff --git a/src/Composer/Platform/Runtime.php b/src/Composer/Platform/Runtime.php new file mode 100644 index 000000000..3c77a9250 --- /dev/null +++ b/src/Composer/Platform/Runtime.php @@ -0,0 +1,108 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Platform; + +class Runtime +{ + /** + * @param string $constant + * @param class-string $class + * @return bool + */ + public function hasConstant($constant, $class = null) + { + return defined(ltrim($class.'::'.$constant, ':')); + } + + /** + * @param bool $constant + * @param class-string $class + * @return mixed + */ + public function getConstant($constant, $class = null) + { + return constant(ltrim($class.'::'.$constant, ':')); + } + + /** + * @param string $fn + * @return bool + */ + public function hasFunction($fn) + { + return function_exists($fn); + } + + /** + * @param callable $callable + * @param array $arguments + * @return mixed + */ + public function invoke($callable, array $arguments = array()) + { + return call_user_func_array($callable, $arguments); + } + + /** + * @param class-string $class + * @return bool + */ + public function hasClass($class) + { + return class_exists($class, false); + } + + /** + * @param class-string $class + * @param array $arguments + * @return object + */ + public function construct($class, array $arguments = array()) + { + if (empty($arguments)) { + return new $class; + } + + $refl = new \ReflectionClass($class); + return $refl->newInstanceArgs($arguments); + } + + /** @return string[] */ + public function getExtensions() + { + return get_loaded_extensions(); + } + + /** + * @param string $extension + * @return string + */ + public function getExtensionVersion($extension) + { + return phpversion($extension); + } + + /** + * @param string $extension + * @return string + */ + public function getExtensionInfo($extension) + { + $reflector = new \ReflectionExtension($extension); + + ob_start(); + $reflector->info(); + + return ob_get_clean(); + } +} diff --git a/src/Composer/Platform/Version.php b/src/Composer/Platform/Version.php new file mode 100644 index 000000000..ab7cae0c4 --- /dev/null +++ b/src/Composer/Platform/Version.php @@ -0,0 +1,104 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Platform; + +/** + * @author Lars Strojny + */ +class Version +{ + /** + * @param string $opensslVersion + * @param bool $isFips + * @return string|null + */ + public static function parseOpenssl($opensslVersion, &$isFips) + { + $isFips = false; + + if (!preg_match('/^(?[0-9.]+)(?[a-z]{0,2})?(?(?:-?(?:dev|pre|alpha|beta|rc|fips)[\d]*)*)?$/', $opensslVersion, $matches)) { + return null; + } + + $isFips = strpos($matches['suffix'], 'fips') !== false; + $suffix = strtr('-'.ltrim($matches['suffix'], '-'), array('-fips' => '', '-pre' => '-alpha')); + $patch = self::convertAlphaVersionToIntVersion($matches['patch']); + + return rtrim($matches['version'].'.'.$patch.$suffix, '-'); + } + + /** + * @param string $libjpegVersion + * @return string|null + */ + public static function parseLibjpeg($libjpegVersion) + { + if (!preg_match('/^(?\d+)(?[a-z]*)$/', $libjpegVersion, $matches)) { + return null; + } + + return $matches['major'].'.'.self::convertAlphaVersionToIntVersion($matches['minor']); + } + + /** + * @param string $zoneinfoVersion + * @return string|null + */ + public static function parseZoneinfoVersion($zoneinfoVersion) + { + if (!preg_match('/^(?\d{4})(?[a-z]*)$/', $zoneinfoVersion, $matches)) { + return null; + } + + return $matches['year'].'.'.self::convertAlphaVersionToIntVersion($matches['revision']); + } + + /** + * "" => 0, "a" => 1, "zg" => 33 + * + * @param string $alpha + * @return int + */ + private static function convertAlphaVersionToIntVersion($alpha) + { + return strlen($alpha) * (-ord('a')+1) + array_sum(array_map('ord', str_split($alpha))); + } + + /** + * @param int $versionId + * @return string + */ + public static function convertLibxpmVersionId($versionId) + { + return self::convertVersionId($versionId, 100); + } + + /** + * @param int $versionId + * @return string + */ + public static function convertOpenldapVersionId($versionId) + { + return self::convertVersionId($versionId, 100); + } + + private static function convertVersionId($versionId, $base) + { + return sprintf( + '%d.%d.%d', + $versionId / ($base * $base), + ($versionId / $base) % $base, + $versionId % $base + ); + } +} diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index a0acb7a61..4f03fc7eb 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -16,6 +16,7 @@ use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\LoaderInterface; +use Composer\Util\Tar; use Composer\Util\Zip; /** @@ -66,7 +67,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveIteratorIterator($directory); - $regex = new \RegexIterator($iterator, '/^.+\.(zip|phar)$/i'); + $regex = new \RegexIterator($iterator, '/^.+\.(zip|phar|tar|gz|tgz)$/i'); foreach ($regex as $file) { /* @var $file \SplFileInfo */ if (!$file->isFile()) { @@ -88,7 +89,26 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito private function getComposerInformation(\SplFileInfo $file) { - $json = Zip::getComposerJson($file->getPathname()); + $json = null; + $fileType = null; + $fileExtension = pathinfo($file->getPathname(), PATHINFO_EXTENSION); + if (in_array($fileExtension, array('gz', 'tar', 'tgz'), true)) { + $fileType = 'tar'; + } else if ($fileExtension === 'zip') { + $fileType = 'zip'; + } else { + throw new \RuntimeException('Files with "'.$fileExtension.'" extensions aren\'t supported. Only ZIP and TAR/TAR.GZ/TGZ archives are supported.'); + } + + try { + if ($fileType === 'tar') { + $json = Tar::getComposerJson($file->getPathname()); + } else { + $json = Zip::getComposerJson($file->getPathname()); + } + } catch (\Exception $exception) { + $this->io->write('Failed loading package '.$file->getPathname().': '.$exception->getMessage(), false, IOInterface::VERBOSE); + } if (null === $json) { return false; @@ -96,7 +116,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); $package['dist'] = array( - 'type' => 'zip', + 'type' => $fileType, 'url' => strtr($file->getPathname(), '\\', '/'), 'shasum' => sha1_file($file->getRealPath()), ); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 2de2fc17d..03d14d876 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -130,6 +130,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$~'); + $this->cache->setReadOnly($config->get('cache-read-only')); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); $this->httpDownloader = $httpDownloader; @@ -1082,7 +1083,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data = $response->decodeJson(); HttpDownloader::outputWarnings($this->io, $this->url, $data); - if ($cacheKey) { + if ($cacheKey && !$this->cache->isReadOnly()) { if ($storeLastModifiedTime) { $lastModifiedDate = $response->getHeader('last-modified'); if ($lastModifiedDate) { @@ -1150,7 +1151,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if (isset($options['http']['header'])) { $options['http']['header'] = (array) $options['http']['header']; } - $options['http']['header'][] = array('If-Modified-Since: '.$lastModifiedTime); + $options['http']['header'][] = 'If-Modified-Since: '.$lastModifiedTime; $response = $this->httpDownloader->get($filename, $options); $json = $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { @@ -1166,7 +1167,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); } - $this->cache->write($cacheKey, $json); + if (!$this->cache->isReadOnly()) { + $this->cache->write($cacheKey, $json); + } return $data; } catch (\Exception $e) { @@ -1213,7 +1216,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $filename = $preFileDownloadEvent->getProcessedUrl(); } - $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); + $options = $this->options; + if ($lastModifiedTime) { + if (isset($options['http']['header'])) { + $options['http']['header'] = (array) $options['http']['header']; + } + $options['http']['header'][] = 'If-Modified-Since: '.$lastModifiedTime; + } $io = $this->io; $url = $this->url; @@ -1243,7 +1252,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data['last-modified'] = $lastModifiedDate; $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); } - $cache->write($cacheKey, $json); + if (!$cache->isReadOnly()) { + $cache->write($cacheKey, $json); + } $repo->freshMetadataUrls[$filename] = true; return $data; diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index c88604a3b..ac6d49525 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -14,14 +14,16 @@ namespace Composer\Repository; use Composer\Composer; use Composer\Package\CompletePackage; +use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Platform\HhvmDetector; +use Composer\Platform\Runtime; +use Composer\Platform\Version; use Composer\Plugin\PluginInterface; -use Composer\Util\ProcessExecutor; +use Composer\Semver\Constraint\Constraint; use Composer\Util\Silencer; -use Composer\Util\Platform; use Composer\XdebugHandler\XdebugHandler; -use Symfony\Component\Process\ExecutableFinder; /** * @author Jordi Boggiano @@ -30,7 +32,9 @@ class PlatformRepository extends ArrayRepository { const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-(?:plugin|runtime)-api)$}iD'; - private static $hhvmVersion; + /** + * @var VersionParser + */ private $versionParser; /** @@ -42,11 +46,13 @@ class PlatformRepository extends ArrayRepository */ private $overrides = array(); - private $process; + private $runtime; + private $hhvmDetector; - public function __construct(array $packages = array(), array $overrides = array(), ProcessExecutor $process = null) + public function __construct(array $packages = array(), array $overrides = array(), Runtime $runtime = null, HhvmDetector $hhvmDetector = null) { - $this->process = $process; + $this->runtime = $runtime ?: new Runtime(); + $this->hhvmDetector = $hhvmDetector ?: new HhvmDetector(); foreach ($overrides as $name => $version) { $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); } @@ -88,10 +94,10 @@ class PlatformRepository extends ArrayRepository $this->addPackage($composerRuntimeApi); try { - $prettyVersion = PHP_VERSION; + $prettyVersion = $this->runtime->getConstant('PHP_VERSION'); $version = $this->versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { - $prettyVersion = preg_replace('#^([^~+-]+).*$#', '$1', PHP_VERSION); + $prettyVersion = preg_replace('#^([^~+-]+).*$#', '$1', $this->runtime->getConstant('PHP_VERSION')); $version = $this->versionParser->normalize($prettyVersion); } @@ -99,19 +105,19 @@ class PlatformRepository extends ArrayRepository $php->setDescription('The PHP interpreter'); $this->addPackage($php); - if (PHP_DEBUG) { + if ($this->runtime->getConstant('PHP_DEBUG')) { $phpdebug = new CompletePackage('php-debug', $version, $prettyVersion); $phpdebug->setDescription('The PHP interpreter, with debugging symbols'); $this->addPackage($phpdebug); } - if (defined('PHP_ZTS') && PHP_ZTS) { + if ($this->runtime->hasConstant('PHP_ZTS') && $this->runtime->getConstant('PHP_ZTS')) { $phpzts = new CompletePackage('php-zts', $version, $prettyVersion); $phpzts->setDescription('The PHP interpreter, with Zend Thread Safety'); $this->addPackage($phpzts); } - if (PHP_INT_SIZE === 8) { + if ($this->runtime->getConstant('PHP_INT_SIZE') === 8) { $php64 = new CompletePackage('php-64bit', $version, $prettyVersion); $php64->setDescription('The PHP interpreter, 64bit'); $this->addPackage($php64); @@ -119,13 +125,13 @@ class PlatformRepository extends ArrayRepository // The AF_INET6 constant is only defined if ext-sockets is available but // IPv6 support might still be available. - if (defined('AF_INET6') || Silencer::call('inet_pton', '::') !== false) { + if ($this->runtime->hasConstant('AF_INET6') || Silencer::call(array($this->runtime, 'invoke'), 'inet_pton', array('::')) !== false) { $phpIpv6 = new CompletePackage('php-ipv6', $version, $prettyVersion); $phpIpv6->setDescription('The PHP interpreter, with IPv6 support'); $this->addPackage($phpIpv6); } - $loadedExtensions = get_loaded_extensions(); + $loadedExtensions = $this->runtime->getExtensions(); // Extensions scanning foreach ($loadedExtensions as $name) { @@ -133,9 +139,7 @@ class PlatformRepository extends ArrayRepository continue; } - $reflExt = new \ReflectionExtension($name); - $prettyVersion = $reflExt->getVersion(); - $this->addExtension($name, $prettyVersion); + $this->addExtension($name, $this->runtime->getExtensionVersion($name)); } // Check for Xdebug in a restarted process @@ -147,112 +151,317 @@ class PlatformRepository extends ArrayRepository // Doing it this way to know that functions or constants exist before // relying on them. foreach ($loadedExtensions as $name) { - $prettyVersion = null; - $description = 'The '.$name.' PHP library'; switch ($name) { + case 'amqp': + $info = $this->runtime->getExtensionInfo($name); + + // librabbitmq version => 0.9.0 + if (preg_match('/^librabbitmq version => (?.+)$/im', $info, $librabbitmqMatches)) { + $this->addLibrary($name.'-librabbitmq', $librabbitmqMatches['version'], 'AMQP librabbitmq version'); + } + + // AMQP protocol version => 0-9-1 + if (preg_match('/^AMQP protocol version => (?.+)$/im', $info, $protocolMatches)) { + $this->addLibrary($name.'-protocol', str_replace('-', '.', $protocolMatches['version']), 'AMQP protocol version'); + } + break; + + case 'bz2': + $info = $this->runtime->getExtensionInfo($name); + + // BZip2 Version => 1.0.6, 6-Sept-2010 + if (preg_match('/^BZip2 Version => (?.*),/im', $info, $matches)) { + $this->addLibrary($name, $matches['version']); + } + break; + case 'curl': - $curlVersion = curl_version(); - $prettyVersion = $curlVersion['version']; + $curlVersion = $this->runtime->invoke('curl_version'); + $this->addLibrary($name, $curlVersion['version']); + + $info = $this->runtime->getExtensionInfo($name); + + // SSL Version => OpenSSL/1.0.1t + if (preg_match('{^SSL Version => (?[^/]+)/(?.+)$}im', $info, $sslMatches)) { + $library = strtolower($sslMatches['library']); + if ($library === 'openssl') { + $parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips); + $this->addLibrary($name.'-openssl'.($isFips ? '-fips': ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', array(), $isFips ? array('curl-openssl'): array()); + } else { + $this->addLibrary($name.'-'.$library, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', array('curl-openssl')); + } + } + + // libSSH Version => libssh2/1.4.3 + if (preg_match('{^libSSH Version => (?[^/]+)/(?.+?)(?:/.*)?$}im', $info, $sshMatches)) { + $this->addLibrary($name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl '.$sshMatches['library'].' version'); + } + + // ZLib Version => 1.2.8 + if (preg_match('{^ZLib Version => (?.+)$}im', $info, $zlibMatches)) { + $this->addLibrary($name.'-zlib', $zlibMatches['version'], 'curl zlib version'); + } break; - case 'iconv': - $prettyVersion = ICONV_VERSION; + case 'date': + $info = $this->runtime->getExtensionInfo($name); + + // timelib version => 2018.03 + if (preg_match('/^timelib version => (?.+)$/im', $info, $timelibMatches)) { + $this->addLibrary($name.'-timelib', $timelibMatches['version'], 'date timelib version'); + } + + // Timezone Database => internal + if (preg_match('/^Timezone Database => (?internal|external)$/im', $info, $zoneinfoSourceMatches)) { + $external = $zoneinfoSourceMatches['source'] === 'external'; + if (preg_match('/^"Olson" Timezone Database Version => (?.+?)(\.system)?$/im', $info, $zoneinfoMatches)) { + // If the timezonedb is provided by ext/timezonedb, register that version as a replacement + if ($external && in_array('timezonedb', $loadedExtensions, true)) { + $this->addLibrary('timezonedb-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date (replaced by timezonedb)', array($name.'-zoneinfo')); + } else { + $this->addLibrary($name.'-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date'); + } + } + } break; - case 'intl': - $name = 'ICU'; - if (defined('INTL_ICU_VERSION')) { - $prettyVersion = INTL_ICU_VERSION; - } else { - $reflector = new \ReflectionExtension('intl'); + case 'fileinfo': + $info = $this->runtime->getExtensionInfo($name); - ob_start(); - $reflector->info(); - $output = ob_get_clean(); + // libmagic => 537 + if (preg_match('/^libmagic => (?.+)$/im', $info, $magicMatches)) { + $this->addLibrary($name.'-libmagic', $magicMatches['version'], 'fileinfo libmagic version'); + } + break; - preg_match('/^ICU version => (.*)$/m', $output, $matches); - $prettyVersion = $matches[1]; + case 'gd': + $this->addLibrary($name, $this->runtime->getConstant('GD_VERSION')); + + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^libJPEG Version => (?.+?)(?: compatible)?$/im', $info, $libjpegMatches)) { + $this->addLibrary($name.'-libjpeg', Version::parseLibjpeg($libjpegMatches['version']), 'libjpeg version for gd'); + } + + if (preg_match('/^libPNG Version => (?.+)$/im', $info, $libpngMatches)) { + $this->addLibrary($name.'-libpng', $libpngMatches['version'], 'libpng version for gd'); + } + + if (preg_match('/^FreeType Version => (?.+)$/im', $info, $freetypeMatches)) { + $this->addLibrary($name.'-freetype', $freetypeMatches['version'], 'freetype version for gd'); + } + + if (preg_match('/^libXpm Version => (?\d+)$/im', $info, $libxpmMatches)) { + $this->addLibrary($name.'-libxpm', Version::convertLibxpmVersionId($libxpmMatches['versionId']), 'libxpm version for gd'); } break; + case 'gmp': + $this->addLibrary($name, $this->runtime->getConstant('GMP_VERSION')); + break; + + case 'iconv': + $this->addLibrary($name, $this->runtime->getConstant('ICONV_VERSION')); + break; + + case 'intl': + $info = $this->runtime->getExtensionInfo($name); + + $description = 'The ICU unicode and globalization support library'; + // Truthy check is for testing only so we can make the condition fail + if ($this->runtime->hasConstant('INTL_ICU_VERSION')) { + $this->addLibrary('icu', $this->runtime->getConstant('INTL_ICU_VERSION'), $description); + } elseif (preg_match('/^ICU version => (?.+)$/im', $info, $matches)) { + $this->addLibrary('icu', $matches['version'], $description); + } + + // ICU TZData version => 2019c + if (preg_match('/^ICU TZData version => (?.*)$/im', $info, $zoneinfoMatches)) { + $this->addLibrary('icu-zoneinfo', Version::parseZoneinfoVersion($zoneinfoMatches['version']), 'zoneinfo ("Olson") database for icu'); + } + + // Add a separate version for the CLDR library version + if ($this->runtime->hasClass('ResourceBundle')) { + $cldrVersion = $this->runtime->invoke(array('ResourceBundle', 'create'), array('root', 'ICUDATA', false))->get('Version'); + $this->addLibrary('icu-cldr', $cldrVersion, 'ICU CLDR project version'); + } + + if ($this->runtime->hasClass('IntlChar')) { + $this->addLibrary('icu-unicode', implode('.', array_slice($this->runtime->invoke(array('IntlChar', 'getUnicodeVersion')), 0, 3)), 'ICU unicode version'); + } + break; + case 'imagick': - $imagick = new \Imagick(); - $imageMagickVersion = $imagick->getVersion(); + $imageMagickVersion = $this->runtime->construct('Imagick')->getVersion(); // 6.x: ImageMagick 6.2.9 08/24/06 Q16 http://www.imagemagick.org // 7.x: ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org - preg_match('/^ImageMagick ([\d.]+)(?:-(\d+))?/', $imageMagickVersion['versionString'], $matches); - if (isset($matches[2])) { - $prettyVersion = "{$matches[1]}.{$matches[2]}"; - } else { - $prettyVersion = $matches[1]; + preg_match('/^ImageMagick (?[\d.]+)(?:-(?\d+))?/', $imageMagickVersion['versionString'], $matches); + $version = $matches['version']; + if (isset($matches['patch'])) { + $version .= '.'.$matches['patch']; + } + + $this->addLibrary($name.'-imagemagick', $version, null, array('imagick')); + break; + + case 'ldap': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^Vendor Version => (?\d+)$/im', $info, $matches) && preg_match('/^Vendor Name => (?.+)$/im', $info, $vendorMatches)) { + $this->addLibrary($name.'-'.strtolower($vendorMatches['vendor']), Version::convertOpenldapVersionId($matches['versionId']), $vendorMatches['vendor'].' version of ldap'); } break; case 'libxml': - $prettyVersion = LIBXML_DOTTED_VERSION; + // ext/dom, ext/simplexml, ext/xmlreader and ext/xmlwriter use the same libxml as the ext/libxml + $libxmlProvides = array_map(function($extension) { + return $extension . '-libxml'; + }, array_intersect($loadedExtensions, array('dom', 'simplexml', 'xml', 'xmlreader', 'xmlwriter'))); + $this->addLibrary($name, $this->runtime->getConstant('LIBXML_DOTTED_VERSION'), 'libxml library version', array(), $libxmlProvides); + + break; + + case 'mbstring': + $info = $this->runtime->getExtensionInfo($name); + + // libmbfl version => 1.3.2 + if (preg_match('/^libmbfl version => (?.+)$/im', $info, $libmbflMatches)) { + $this->addLibrary($name.'-libmbfl', $libmbflMatches['version'], 'mbstring libmbfl version'); + } + + if ($this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) { + $this->addLibrary($name.'-oniguruma', $this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); + + // Multibyte regex (oniguruma) version => 5.9.5 + // oniguruma version => 6.9.0 + } elseif (preg_match('/^(?:oniguruma|Multibyte regex \(oniguruma\)) version => (?.+)$/im', $info, $onigurumaMatches)) { + $this->addLibrary($name.'-oniguruma', $onigurumaMatches['version'], 'mbstring oniguruma version'); + } + + break; + + case 'memcached': + $info = $this->runtime->getExtensionInfo($name); + + // libmemcached version => 1.0.18 + if (preg_match('/^libmemcached version => (?.+)$/im', $info, $matches)) { + $this->addLibrary($name.'-libmemcached', $matches['version'], 'libmemcached version'); + } break; case 'openssl': - $prettyVersion = preg_replace_callback('{^(?:OpenSSL|LibreSSL)?\s*([0-9.]+)([a-z]*).*}i', function ($match) { - if (empty($match[2])) { - return $match[1]; - } - - // OpenSSL versions add another letter when they reach Z. - // e.g. OpenSSL 0.9.8zh 3 Dec 2015 - - if (!preg_match('{^z*[a-z]$}', $match[2])) { - // 0.9.8abc is garbage - return 0; - } - - $len = strlen($match[2]); - $patchVersion = ($len - 1) * 26; // All Z - $patchVersion += ord($match[2][$len - 1]) - 96; - - return $match[1].'.'.$patchVersion; - }, OPENSSL_VERSION_TEXT); - - $description = OPENSSL_VERSION_TEXT; + // OpenSSL 1.1.1g 21 Apr 2020 + if (preg_match('{^(?:OpenSSL|LibreSSL)?\s*(?\S+)}i', $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), $matches)) { + $parsedVersion = Version::parseOpenssl($matches['version'], $isFips); + $this->addLibrary($name.($isFips ? '-fips' : ''), $parsedVersion, $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), array(), $isFips ? array($name) : array()); + } break; case 'pcre': - $prettyVersion = preg_replace('{^(\S+).*}', '$1', PCRE_VERSION); + $this->addLibrary($name, preg_replace('{^(\S+).*}', '$1', $this->runtime->getConstant('PCRE_VERSION'))); + + $info = $this->runtime->getExtensionInfo($name); + + // PCRE Unicode Version => 12.1.0 + if (preg_match('/^PCRE Unicode Version => (?.+)$/im', $info, $pcreUnicodeMatches)) { + $this->addLibrary($name.'-unicode', $pcreUnicodeMatches['version'], 'PCRE Unicode version support'); + } + break; - case 'uuid': - $prettyVersion = phpversion('uuid'); + case 'mysqlnd': + case 'pdo_mysql': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^(?:Client API version|Version) => mysqlnd (?.+?) /mi', $info, $matches)) { + $this->addLibrary($name.'-mysqlnd', $matches['version'], 'mysqlnd library version for '.$name); + } + break; + + case 'mongodb': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^libmongoc bundled version => (?.+)$/im', $info, $libmongocMatches)) { + $this->addLibrary($name.'-libmongoc', $libmongocMatches['version'], 'libmongoc version of mongodb'); + } + + if (preg_match('/^libbson bundled version => (?.+)$/im', $info, $libbsonMatches)) { + $this->addLibrary($name.'-libbson', $libbsonMatches['version'], 'libbson version of mongodb'); + } + break; + + case 'pgsql': + case 'pdo_pgsql': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^PostgreSQL\(libpq\) Version => (?.*)$/im', $info, $matches)) { + $this->addLibrary($name.'-libpq', $matches['version'], 'libpq for '.$name); + } + break; + + case 'libsodium': + case 'sodium': + $this->addLibrary('libsodium', $this->runtime->getConstant('SODIUM_LIBRARY_VERSION')); + break; + + case 'sqlite3': + case 'pdo_sqlite': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^SQLite Library => (?.+)$/im', $info, $matches)) { + $this->addLibrary($name.'-sqlite', $matches['version']); + } + break; + + case 'ssh2': + $info = $this->runtime->getExtensionInfo($name); + + if (preg_match('/^libssh2 version => (?.+)$/im', $info, $matches)) { + $this->addLibrary($name.'-libssh2', $matches['version']); + } break; case 'xsl': - $prettyVersion = LIBXSLT_DOTTED_VERSION; + $this->addLibrary('libxslt', $this->runtime->getConstant('LIBXSLT_DOTTED_VERSION'), null, array('xsl')); + + $info = $this->runtime->getExtensionInfo('xsl'); + if (preg_match('/^libxslt compiled against libxml Version => (?.+)$/im', $info, $matches)) { + $this->addLibrary('libxslt-libxml', $matches['version'], 'libxml version libxslt is compiled against'); + } + break; + + case 'yaml': + $info = $this->runtime->getExtensionInfo('yaml'); + + if (preg_match('/^LibYAML Version => (?.+)$/im', $info, $matches)) { + $this->addLibrary($name.'-libyaml', $matches['version'], 'libyaml version of yaml'); + } break; case 'zip': - if (defined('ZipArchive::LIBZIP_VERSION')) { - $prettyVersion = \ZipArchive::LIBZIP_VERSION; - } else { - continue 2; + if ($this->runtime->hasConstant('LIBZIP_VERSION', 'ZipArchive')) { + $this->addLibrary($name.'-libzip', $this->runtime->getConstant('LIBZIP_VERSION','ZipArchive'), null, array('zip')); } + break; + + case 'zlib': + if ($this->runtime->hasConstant('ZLIB_VERSION')) { + $this->addLibrary($name, $this->runtime->getConstant('ZLIB_VERSION')); + + // Linked Version => 1.2.8 + } elseif (preg_match('/^Linked Version => (?.+)$/im', $this->runtime->getExtensionInfo($name), $matches)) { + $this->addLibrary($name, $matches['version']); + } + break; default: - // None handled extensions have no special cases, skip - continue 2; + break; } - - try { - $version = $this->versionParser->normalize($prettyVersion); - } catch (\UnexpectedValueException $e) { - continue; - } - - $lib = new CompletePackage('lib-'.$name, $version, $prettyVersion); - $lib->setDescription($description); - $this->addPackage($lib); } - if ($hhvmVersion = self::getHHVMVersion($this->process)) { + $hhvmVersion = $this->hhvmDetector->getVersion(); + if ($hhvmVersion) { try { $prettyVersion = $hhvmVersion; $version = $this->versionParser->normalize($prettyVersion); @@ -337,6 +546,11 @@ class PlatformRepository extends ArrayRepository $packageName = $this->buildPackageName($name); $ext = new CompletePackage($packageName, $version, $prettyVersion); $ext->setDescription('The '.$name.' PHP extension'.$extraDescription); + + if ($name === 'uuid') { + $ext->setReplaces(array(new Link('ext-uuid', 'lib-uuid', new Constraint('=', $version)))); + } + $this->addPackage($ext); } @@ -346,31 +560,37 @@ class PlatformRepository extends ArrayRepository */ private function buildPackageName($name) { - return 'ext-' . str_replace(' ', '-', $name); + return 'ext-' . str_replace(' ', '-', strtolower($name)); } - private static function getHHVMVersion(ProcessExecutor $process = null) + /** + * @param string $name + * @param string $prettyVersion + * @param string|null $description + * @param string[] $replaces + * @param string[] $provides + */ + private function addLibrary($name, $prettyVersion, $description = null, array $replaces = array(), array $provides = array()) { - if (null !== self::$hhvmVersion) { - return self::$hhvmVersion ?: null; + try { + $version = $this->versionParser->normalize($prettyVersion); + } catch (\UnexpectedValueException $e) { + return; } - self::$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; - if (self::$hhvmVersion === null && !Platform::isWindows()) { - self::$hhvmVersion = false; - $finder = new ExecutableFinder(); - $hhvmPath = $finder->find('hhvm'); - if ($hhvmPath !== null) { - $process = $process ?: new ProcessExecutor(); - $exitCode = $process->execute( - ProcessExecutor::escape($hhvmPath). - ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', - self::$hhvmVersion - ); - if ($exitCode !== 0) { - self::$hhvmVersion = false; - } - } + if ($description === null) { + $description = 'The '.$name.' library'; } + + $lib = new CompletePackage('lib-'.$name, $version, $prettyVersion); + $lib->setDescription($description); + + $links = function ($alias) use ($name, $version) { + return new Link('lib-'.$name, 'lib-'.$alias, new Constraint('=', $version)); + }; + $lib->setReplaces(array_map($links, $replaces)); + $lib->setProvides(array_map($links, $provides)); + + $this->addPackage($lib); } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 9417dec97..04db0becd 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -193,7 +193,7 @@ class RepositorySet } } - return $candidates; + return $result; } public function getProviders($packageName) diff --git a/src/Composer/Repository/Vcs/BitbucketDriver.php b/src/Composer/Repository/Vcs/BitbucketDriver.php index 22180b55d..c1781342f 100644 --- a/src/Composer/Repository/Vcs/BitbucketDriver.php +++ b/src/Composer/Repository/Vcs/BitbucketDriver.php @@ -60,6 +60,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository, )) ); + $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** @@ -120,10 +121,14 @@ abstract class BitbucketDriver extends VcsDriver if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { - return $this->infoCache[$identifier] = JsonFile::parseJson($res); - } + $composer = JsonFile::parseJson($res); + } else { + $composer = $this->getBaseComposerInformation($identifier); - $composer = $this->getBaseComposerInformation($identifier); + if ($this->shouldCache($identifier)) { + $this->cache->write($identifier, json_encode($composer)); + } + } if ($composer) { // specials for bitbucket @@ -174,10 +179,6 @@ abstract class BitbucketDriver extends VcsDriver } $this->infoCache[$identifier] = $composer; - - if ($this->shouldCache($identifier)) { - $this->cache->write($identifier, json_encode($composer)); - } } return $this->infoCache[$identifier]; diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index c7672c619..499a5a9df 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -75,6 +75,7 @@ class GitDriver extends VcsDriver $this->getBranches(); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $cacheUrl)); + $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 0a97123c0..bbbbd6ecb 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -58,6 +58,7 @@ class GitHubDriver extends VcsDriver $this->originUrl = 'github.com'; } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); + $this->cache->setReadOnly($this->config->get('cache-read-only')); if ( $this->config->get('use-github-api') === false || (isset($this->repoConfig['no-api']) && $this->repoConfig['no-api'] ) ){ $this->setupGitDriver($this->url); @@ -151,10 +152,14 @@ class GitHubDriver extends VcsDriver if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { - return $this->infoCache[$identifier] = JsonFile::parseJson($res); - } + $composer = JsonFile::parseJson($res); + } else { + $composer = $this->getBaseComposerInformation($identifier); - $composer = $this->getBaseComposerInformation($identifier); + if ($this->shouldCache($identifier)) { + $this->cache->write($identifier, json_encode($composer)); + } + } if ($composer) { // specials for github @@ -173,10 +178,6 @@ class GitHubDriver extends VcsDriver } } - if ($this->shouldCache($identifier)) { - $this->cache->write($identifier, json_encode($composer)); - } - $this->infoCache[$identifier] = $composer; } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 8f0ed6f0f..2987b3b94 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -105,6 +105,7 @@ class GitLabDriver extends VcsDriver $this->repository = preg_replace('#(\.git)$#', '', $match['repo']); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository); + $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->fetchProject(); } @@ -131,10 +132,14 @@ class GitLabDriver extends VcsDriver if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { - return $this->infoCache[$identifier] = JsonFile::parseJson($res); - } + $composer = JsonFile::parseJson($res); + } else { + $composer = $this->getBaseComposerInformation($identifier); - $composer = $this->getBaseComposerInformation($identifier); + if ($this->shouldCache($identifier)) { + $this->cache->write($identifier, json_encode($composer)); + } + } if ($composer) { // specials for gitlab (this data is only available if authentication is provided) @@ -146,10 +151,6 @@ class GitLabDriver extends VcsDriver } } - if ($this->shouldCache($identifier)) { - $this->cache->write($identifier, json_encode($composer)); - } - $this->infoCache[$identifier] = $composer; } @@ -448,7 +449,7 @@ class GitLabDriver extends VcsDriver if (!$moreThanGuestAccess) { $this->io->writeError('GitLab token with Guest only access detected'); - return $this->attemptCloneFallback(); + return $this->attemptCloneFallback(); } } diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index 097103487..87c9781b6 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -78,6 +78,7 @@ class SvnDriver extends VcsDriver } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->baseUrl)); + $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->getBranches(); $this->getTags(); diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 37946da23..f174f8527 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -81,7 +81,7 @@ abstract class VcsDriver implements VcsDriverInterface */ protected function shouldCache($identifier) { - return $this->cache && preg_match('{[a-f0-9]{40}}i', $identifier); + return $this->cache && preg_match('{^[a-f0-9]{40}$}iD', $identifier); } /** diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 24f85053c..08c831b98 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -265,6 +265,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt if ($e->getCode() === 404) { $this->emptyReferences[] = $identifier; } + if ($e->getCode() === 401 || $e->getCode() === 403) { + throw $e; + } } if ($isVeryVerbose) { $this->io->writeError('Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found (' . $e->getCode() . ' HTTP status code)' : $e->getMessage()).''); @@ -350,6 +353,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt if ($e->getCode() === 404) { $this->emptyReferences[] = $identifier; } + if ($e->getCode() === 401 || $e->getCode() === 403) { + throw $e; + } if ($isVeryVerbose) { $this->io->writeError('Skipped branch '.$branch.', no composer file was found (' . $e->getCode() . ' HTTP status code)'); } diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index 6d1379d41..21b48d9e5 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -26,6 +26,7 @@ class Versions private $httpDownloader; private $config; private $channel; + private $versionsData; public function __construct(Config $config, HttpDownloader $httpDownloader) { @@ -63,12 +64,7 @@ class Versions public function getLatest($channel = null) { - if ($this->config->get('disable-tls') === true) { - $protocol = 'http'; - } else { - $protocol = 'https'; - } - $versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); + $versions = $this->getVersionsData(); foreach ($versions[$channel ?: $this->getChannel()] as $version) { if ($version['min-php'] <= PHP_VERSION_ID) { @@ -76,6 +72,21 @@ class Versions } } - throw new \LogicException('There is no version of Composer available for your PHP version ('.PHP_VERSION.')'); + throw new \UnexpectedValueException('There is no version of Composer available for your PHP version ('.PHP_VERSION.')'); + } + + private function getVersionsData() + { + if (!$this->versionsData) { + if ($this->config->get('disable-tls') === true) { + $protocol = 'http'; + } else { + $protocol = 'https'; + } + + $this->versionsData = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); + } + + return $this->versionsData; } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 257a29115..f3b2e1496 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -13,6 +13,7 @@ namespace Composer\Util\Http; use Composer\Config; +use Composer\Downloader\MaxFileSizeExceededException; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; @@ -56,6 +57,8 @@ class CurlDownloader 'ssl' => array( 'cafile' => CURLOPT_CAINFO, 'capath' => CURLOPT_CAPATH, + 'verify_peer' => CURLOPT_SSL_VERIFYPEER, + 'verify_peer_name' => CURLOPT_SSL_VERIFYHOST, ), ); @@ -178,7 +181,11 @@ class CurlDownloader foreach (self::$options as $type => $curlOptions) { foreach ($curlOptions as $name => $curlOption) { if (isset($options[$type][$name])) { - curl_setopt($curlHandle, $curlOption, $options[$type][$name]); + if ($type === 'ssl' && $name === 'verify_peer_name') { + curl_setopt($curlHandle, $curlOption, $options[$type][$name] === true ? 2 : $options[$type][$name]); + } else { + curl_setopt($curlHandle, $curlOption, $options[$type][$name]); + } } } } @@ -359,6 +366,18 @@ class CurlDownloader $previousProgress = $this->jobs[$i]['progress']; $this->jobs[$i]['progress'] = $progress; + if (isset($this->jobs[$i]['options']['max_file_size'])) { + // Compare max_file_size with the content-length header this value will be -1 until the header is parsed + if ($this->jobs[$i]['options']['max_file_size'] < $progress['download_content_length']) { + throw new MaxFileSizeExceededException('Maximum allowed download size reached. Content-length header indicates ' . $progress['download_content_length'] . ' bytes. Allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes'); + } + + // Compare max_file_size with the download size in bytes + if ($this->jobs[$i]['options']['max_file_size'] < $progress['size_download']) { + throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . $progress['size_download'] . ' of allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes'); + } + } + // TODO //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 615de0adb..9ed465355 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -13,6 +13,7 @@ namespace Composer\Util; use Composer\Config; +use Composer\Downloader\MaxFileSizeExceededException; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; @@ -244,6 +245,12 @@ class RemoteFilesystem $degradedPackagist = true; } + $maxFileSize = null; + if (isset($options['max_file_size'])) { + $maxFileSize = $options['max_file_size']; + unset($options['max_file_size']); + } + $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); $actualContextOptions = stream_context_get_options($ctx); @@ -273,7 +280,7 @@ class RemoteFilesystem }); $http_response_header = array(); try { - $result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header); + $result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header, $maxFileSize); if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); @@ -532,23 +539,34 @@ class RemoteFilesystem /** * Get contents of remote URL. * - * @param string $originUrl The origin URL - * @param string $fileUrl The file URL - * @param resource $context The stream context + * @param string $originUrl The origin URL + * @param string $fileUrl The file URL + * @param resource $context The stream context + * @param int $maxFileSize The maximum allowed file size * * @return string|false The response contents or false on failure */ - protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null) + protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null, $maxFileSize = null) { $result = false; try { $e = null; - $result = file_get_contents($fileUrl, false, $context); + if ($maxFileSize !== null) { + $result = file_get_contents($fileUrl, false, $context, 0, $maxFileSize); + } else { + // passing `null` to file_get_contents will convert `null` to `0` and return 0 bytes + $result = file_get_contents($fileUrl, false, $context); + } + } catch (\Throwable $e) { } catch (\Exception $e) { } + if ($maxFileSize !== null && Platform::strlen($result) >= $maxFileSize) { + throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' . $maxFileSize . ' bytes'); + } + $responseHeaders = isset($http_response_header) ? $http_response_header : array(); if (null !== $e) { diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 09291a908..e91707ab3 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -29,6 +29,7 @@ final class StreamContextFactory * Creates a context supporting HTTP proxies * * @param string $url URL the context is to be used for + * @psalm-param array{http: array{follow_location?: int, max_redirects?: int, header?: string|array}} $defaultOptions * @param array $defaultOptions Options to merge with the default * @param array $defaultParams Parameters to specify on the context * @throws \RuntimeException if https proxy required and OpenSSL uninstalled @@ -56,7 +57,8 @@ final class StreamContextFactory /** * @param string $url * @param array $options - * @return array ['http' => ['header' => [...], 'proxy' => '..', 'request_fulluri' => bool]] formatted as a stream context array + * @psalm-return array{http:{header: string[], proxy?: string, request_fulluri: bool}, ssl: array} + * @return array formatted as a stream context array */ public static function initOptions($url, array $options) { diff --git a/src/Composer/Util/Tar.php b/src/Composer/Util/Tar.php new file mode 100644 index 000000000..cae69c906 --- /dev/null +++ b/src/Composer/Util/Tar.php @@ -0,0 +1,68 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * @author Wissem Riahi + */ +class Tar +{ + /** + * @param string $pathToArchive + * + * @return string|null + */ + public static function getComposerJson($pathToArchive) + { + $phar = new \PharData($pathToArchive); + + if (!$phar->valid()) { + return null; + } + + return self::extractComposerJsonFromFolder($phar); + } + + /** + * @param \PharData $phar + * + * @throws \RuntimeException + * + * @return string + */ + private static function extractComposerJsonFromFolder(\PharData $phar) + { + if (isset($phar['composer.json'])) { + return $phar['composer.json']->getContent(); + } + + $topLevelPaths = array(); + foreach ($phar as $folderFile) { + $name = $folderFile->getBasename(); + + if ($folderFile->isDir()) { + $topLevelPaths[$name] = true; + if (\count($topLevelPaths) > 1) { + throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); + } + } + } + + $composerJsonPath = key($topLevelPaths).'/composer.json'; + if ($topLevelPaths && isset($phar[$composerJsonPath])) { + return $phar[$composerJsonPath]->getContent(); + } + + throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory'); + } +} diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index ab10d5bbf..ad6617edf 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -66,42 +66,44 @@ class Zip * * @param \ZipArchive $zip * @param string $filename + * @throws \RuntimeException * - * @return bool|int + * @return int */ private static function locateFile(\ZipArchive $zip, $filename) { - $indexOfShortestMatch = false; - $lengthOfShortestMatch = -1; + // return root composer.json if it is there and is a file + if (false !== ($index = $zip->locateName($filename)) && $zip->getFromIndex($index) !== false) { + return $index; + } + $topLevelPaths = array(); for ($i = 0; $i < $zip->numFiles; $i++) { - $stat = $zip->statIndex($i); - if (strcmp(basename($stat['name']), $filename) === 0) { - $directoryName = dirname($stat['name']); - if ($directoryName === '.') { - //if composer.json is in root directory - //it has to be the one to use. - return $i; - } + $name = $zip->getNameIndex($i); + $dirname = dirname($name); - if (strpos($directoryName, '\\') !== false || - strpos($directoryName, '/') !== false) { - //composer.json files below first directory are rejected - continue; + // handle archives with proper TOC + if ($dirname === '.') { + $topLevelPaths[$name] = true; + if (\count($topLevelPaths) > 1) { + throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); } + continue; + } - $length = strlen($stat['name']); - if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { - //Check it's not a directory. - $contents = $zip->getFromIndex($i); - if ($contents !== false) { - $indexOfShortestMatch = $i; - $lengthOfShortestMatch = $length; - } + // handle archives which do not have a TOC record for the directory itself + if (false === strpos('\\', $dirname) && false === strpos('/', $dirname)) { + $topLevelPaths[$dirname.'/'] = true; + if (\count($topLevelPaths) > 1) { + throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); } } } - return $indexOfShortestMatch; + if ($topLevelPaths && false !== ($index = $zip->locateName(key($topLevelPaths).$filename)) && $zip->getFromIndex($index) !== false) { + return $index; + } + + throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory'); } } diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 6618618e1..97b6db9ac 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -390,7 +390,7 @@ class GitDownloaderTest extends TestCase public function testUpdate() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -421,7 +421,7 @@ class GitDownloaderTest extends TestCase public function testUpdateWithNewRepoUrl() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -496,8 +496,8 @@ composer https://github.com/old/url (push) */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); - $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'git@github.com:composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand2 = $this->winCompat("(git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'git@github.com:composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -540,8 +540,8 @@ composer https://github.com/old/url (push) public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."'"); - $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); + $expectedFirstGitUpdateCommand = $this->winCompat("(git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."'"); + $expectedSecondGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test new file mode 100644 index 000000000..b5d195a46 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test @@ -0,0 +1,77 @@ +--TEST-- +Partial update from lock file with root aliases should work +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/dep", "version": "1.0.0", "require": { "c/aliased": "2.0.0" } }, + { "name": "b/dep", "version": "1.0.0", "require": { "c/aliased": "1.0.0" } }, + { "name": "c/aliased", "version": "1.0.0" }, + { "name": "c/aliased", "version": "2.0.0" } + ] + } + ], + "require": { + "a/dep": "*", + "b/dep": "*", + "c/aliased": "1.0.0 as 2.0.0" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/dep", "version": "1.0.0", "require": { "c/aliased": "2.0.0" } }, + { "name": "b/dep", "version": "1.0.0", "require": { "c/aliased": "1.0.0" } }, + { "name": "c/aliased", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [ + { + "package": "c/aliased", + "version": "1.0.0.0", + "alias": "2.0.0", + "alias_normalized": "2.0.0.0" + } + ], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/dep", "version": "1.0.0", "require": { "c/aliased": "2.0.0" } }, + { "name": "b/dep", "version": "1.0.0", "require": { "c/aliased": "1.0.0" } }, + { "name": "c/aliased", "version": "1.0.0" } +] +--RUN-- +update --lock +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/dep", "version": "1.0.0", "require": { "c/aliased": "2.0.0" }, "type": "library" }, + { "name": "b/dep", "version": "1.0.0", "require": { "c/aliased": "1.0.0" }, "type": "library" }, + { "name": "c/aliased", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [ + { + "package": "c/aliased", + "version": "1.0.0.0", + "alias": "2.0.0", + "alias_normalized": "2.0.0.0" + } + ], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Marking c/aliased (2.0.0) as installed, alias of c/aliased (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities4.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities4.test new file mode 100644 index 000000000..9ef597daa --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities4.test @@ -0,0 +1,59 @@ +--TEST-- +Packages found in a higher priority repository take precedence even if they are not found in the requested version case #2 +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "ruflin/elastica", + "version": "dev-outdated-branch" + } + ] + }, + { + "type": "package", + "package": [ + { + "name": "friendsofsymfony/elastica-bundle", + "version": "dev-foobar-master", + "require": { + "ruflin/elastica": "2.*" + } + } + ] + }, + { + "type": "package", + "package": [ + { + "name": "ruflin/elastica", + "version": "2.0.0" + }, + { + "name": "ruflin/elastica", + "version": "2.0.1" + } + ] + } + ], + "require": { + "friendsofsymfony/elastica-bundle": "dev-foobar-master" + } +} + +--RUN-- +update +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires friendsofsymfony/elastica-bundle dev-foobar-master -> satisfiable by friendsofsymfony/elastica-bundle[dev-foobar-master]. + - friendsofsymfony/elastica-bundle dev-foobar-master requires ruflin/elastica 2.* -> satisfiable by ruflin/elastica[2.0.0, 2.0.1] from package repo (defining 2 packages) but ruflin/elastica[dev-outdated-branch] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance. + +--EXPECT-- +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities5.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities5.test new file mode 100644 index 000000000..7a196988e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities5.test @@ -0,0 +1,35 @@ +--TEST-- +Packages found in a higher priority repository take precedence even if they are not found in the requested version case #3 +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "2.0.0-dev" } + ] + }, + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "2.0.0" } + ] + } + ], + "require": { + "foo/a": "2.*" + } +} +--RUN-- +update +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[2.0.0-dev] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your minimum-stability and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance. + +--EXPECT-- +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Platform/HhvmDetectorTest.php b/tests/Composer/Test/Platform/HhvmDetectorTest.php new file mode 100644 index 000000000..600ef7193 --- /dev/null +++ b/tests/Composer/Test/Platform/HhvmDetectorTest.php @@ -0,0 +1,99 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Platform; + +use Composer\Platform\HhvmDetector; +use Composer\Test\TestCase; +use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; +use Symfony\Component\Process\ExecutableFinder; + +class HhvmDetectorTest extends TestCase +{ + private $hhvmDetector; + + protected function setUp() + { + $this->hhvmDetector = new HhvmDetector(); + $this->hhvmDetector->reset(); + } + + public function testHHVMVersionWhenExecutingInHHVM() + { + if (!defined('HHVM_VERSION_ID')) { + self::markTestSkipped('Not running with HHVM'); + return; + } + $version = $this->hhvmDetector->getVersion(); + self::assertSame(self::versionIdToVersion(), $version); + } + + public function testHHVMVersionWhenExecutingInPHP() + { + if (defined('HHVM_VERSION_ID')) { + self::markTestSkipped('Running with HHVM'); + return; + } + if (PHP_VERSION_ID < 50400) { + self::markTestSkipped('Test only works on PHP 5.4+'); + return; + } + if (Platform::isWindows()) { + self::markTestSkipped('Test does not run on Windows'); + return; + } + $finder = new ExecutableFinder(); + $hhvm = $finder->find('hhvm'); + if ($hhvm === null) { + self::markTestSkipped('HHVM is not installed'); + } + + $detectedVersion = $this->hhvmDetector->getVersion(); + self::assertNotNull($detectedVersion, 'Failed to detect HHVM version'); + + $process = new ProcessExecutor(); + $exitCode = $process->execute( + ProcessExecutor::escape($hhvm). + ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', + $version + ); + self::assertSame(0, $exitCode); + + self::assertSame(self::getVersionParser()->normalize($version), self::getVersionParser()->normalize($detectedVersion)); + } + + /** @runInSeparateProcess */ + public function testHHVMVersionWhenRunningInHHVMWithMockedConstant() + { + if (!defined('HHVM_VERSION_ID')) { + define('HHVM_VERSION', '2.2.1'); + define('HHVM_VERSION_ID', 20201); + } + $version = $this->hhvmDetector->getVersion(); + self::assertSame(self::getVersionParser()->normalize(self::versionIdToVersion()), self::getVersionParser()->normalize($version)); + } + + private static function versionIdToVersion() + { + if (!defined('HHVM_VERSION_ID')) { + return null; + } + + return sprintf( + '%d.%d.%d', + HHVM_VERSION_ID / 10000, + (HHVM_VERSION_ID / 100) % 100, + HHVM_VERSION_ID % 100 + ); + } +} diff --git a/tests/Composer/Test/Platform/VersionTest.php b/tests/Composer/Test/Platform/VersionTest.php new file mode 100644 index 000000000..5d5452143 --- /dev/null +++ b/tests/Composer/Test/Platform/VersionTest.php @@ -0,0 +1,131 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Platform; + +use Composer\Platform\Version; +use Composer\Test\TestCase; + +/** + * @author Lars Strojny + */ +class VersionTest extends TestCase +{ + /** + * Create normalized test data set + * + * 1) Clone OpenSSL repository + * 2) git log --pretty=%h --all -- crypto/opensslv.h include/openssl/opensslv.h | while read hash ; do (git show $hash:crypto/opensslv.h; git show $hash:include/openssl/opensslv.h) | grep "define OPENSSL_VERSION_TEXT" ; done > versions.txt + * 3) cat versions.txt | awk -F "OpenSSL " '{print $2}' | awk -F " " '{print $1}' | sed -e "s:\([0-9]*\.[0-9]*\.[0-9]*\):1.2.3:g" -e "s:1\.2\.3[a-z]\(-.*\)\{0,1\}$:1.2.3a\1:g" -e "s:1\.2\.3[a-z]\{2\}\(-.*\)\{0,1\}$:1.2.3zh\1:g" -e "s:beta[0-9]:beta3:g" -e "s:pre[0-9]*:pre2:g" | sort | uniq + */ + public static function getOpenSslVersions() + { + return array( + // Generated + array('1.2.3', '1.2.3.0'), + array('1.2.3-beta3', '1.2.3.0-beta3'), + array('1.2.3-beta3-dev', '1.2.3.0-beta3-dev'), + array('1.2.3-beta3-fips', '1.2.3.0-beta3', true), + array('1.2.3-beta3-fips-dev', '1.2.3.0-beta3-dev', true), + array('1.2.3-dev', '1.2.3.0-dev'), + array('1.2.3-fips', '1.2.3.0', true), + array('1.2.3-fips-beta3', '1.2.3.0-beta3', true), + array('1.2.3-fips-beta3-dev', '1.2.3.0-beta3-dev', true), + array('1.2.3-fips-dev', '1.2.3.0-dev', true), + array('1.2.3-pre2', '1.2.3.0-alpha2'), + array('1.2.3-pre2-dev', '1.2.3.0-alpha2-dev'), + array('1.2.3-pre2-fips', '1.2.3.0-alpha2', true), + array('1.2.3-pre2-fips-dev', '1.2.3.0-alpha2-dev', true), + array('1.2.3a', '1.2.3.1'), + array('1.2.3a-beta3','1.2.3.1-beta3'), + array('1.2.3a-beta3-dev', '1.2.3.1-beta3-dev'), + array('1.2.3a-dev', '1.2.3.1-dev'), + array('1.2.3a-dev-fips', '1.2.3.1-dev', true), + array('1.2.3a-fips', '1.2.3.1', true), + array('1.2.3a-fips-beta3', '1.2.3.1-beta3', true), + array('1.2.3a-fips-dev', '1.2.3.1-dev', true), + array('1.2.3beta3', '1.2.3.0-beta3'), + array('1.2.3beta3-dev', '1.2.3.0-beta3-dev'), + array('1.2.3zh', '1.2.3.34'), + array('1.2.3zh-dev', '1.2.3.34-dev'), + array('1.2.3zh-fips', '1.2.3.34',true), + array('1.2.3zh-fips-dev', '1.2.3.34-dev', true), + // Additional cases + array('1.2.3zh-fips-rc3', '1.2.3.34-rc3', true, '1.2.3.34-RC3'), + array('1.2.3zh-alpha10-fips', '1.2.3.34-alpha10', true), + // Check that alphabetical patch levels overflow correctly + array('1.2.3', '1.2.3.0'), + array('1.2.3a', '1.2.3.1'), + array('1.2.3z', '1.2.3.26'), + array('1.2.3za', '1.2.3.27'), + array('1.2.3zy', '1.2.3.51'), + array('1.2.3zz', '1.2.3.52'), + ); + } + + /** + * @dataProvider getOpenSslVersions + * @param string $input + * @param string $parsedVersion + * @param bool $fipsExpected + * @param string|null $normalizedVersion + */ + public function testParseOpensslVersions($input, $parsedVersion, $fipsExpected = false, $normalizedVersion = null) + { + self::assertSame($parsedVersion, Version::parseOpenssl($input, $isFips)); + self::assertSame($fipsExpected, $isFips); + + $normalizedVersion = $normalizedVersion ? $normalizedVersion : $parsedVersion; + self::assertSame($normalizedVersion, $this->getVersionParser()->normalize($parsedVersion)); + } + + public function getLibJpegVersions() + { + return array( + array('9', '9.0'), + array('9a', '9.1'), + array('9b', '9.2'), + // Never seen in the wild, just for overflow correctness + array('9za', '9.27'), + ); + } + + /** + * @dataProvider getLibJpegVersions + * @param string $input + * @param string $parsedVersion + */ + public function testParseLibjpegVersion($input, $parsedVersion) + { + self::assertSame($parsedVersion, Version::parseLibjpeg($input)); + } + + public function getZoneinfoVersions() + { + return array( + array('2019c', '2019.3'), + array('2020a', '2020.1'), + // Never happened so far but fixate overflow behavior + array('2020za', '2020.27'), + ); + } + + /** + * @dataProvider getZoneinfoVersions + * @param string $input + * @param string $parsedVersion + */ + public function testParseZoneinfoVersion($input, $parsedVersion) + { + self::assertSame($parsedVersion, Version::parseZoneinfoVersion($input)); + } +} diff --git a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php index 506a033c4..ad7a69009 100644 --- a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php @@ -36,6 +36,7 @@ class ArtifactRepositoryTest extends TestCase 'vendor1/package2-4.3.2', 'vendor3/package1-5.4.3', 'test/jsonInRoot-1.0.0', + 'test/jsonInRootTarFile-1.0.0', 'test/jsonInFirstLevel-1.0.0', //The files not-an-artifact.zip and jsonSecondLevel are not valid //artifacts and do not get detected. @@ -52,6 +53,13 @@ class ArtifactRepositoryTest extends TestCase sort($foundPackages); $this->assertSame($expectedPackages, $foundPackages); + + $tarPackage = array_filter($repo->getPackages(), function (BasePackage $package) { + return $package->getPrettyName() === 'test/jsonInRootTarFile'; + }); + $this->assertCount(1, $tarPackage); + $tarPackage = array_pop($tarPackage); + $this->assertSame('tar', $tarPackage->getDistType()); } public function testAbsoluteRepoUrlCreatesAbsoluteUrlPackages() diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip index e94843eb6..585b4f7ea 100644 Binary files a/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip and b/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip index 498037464..653b60095 100644 Binary files a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevelWithExtraFirstLevel.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevelWithExtraFirstLevel.zip new file mode 100644 index 000000000..b400918b9 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevelWithExtraFirstLevel.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz new file mode 100644 index 000000000..7d2938703 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip index 3e788dcc2..0979dcb16 100644 Binary files a/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip and b/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip differ diff --git a/tests/Composer/Test/Repository/PlatformRepositoryTest.php b/tests/Composer/Test/Repository/PlatformRepositoryTest.php index 519aadb31..2da09c1dd 100644 --- a/tests/Composer/Test/Repository/PlatformRepositoryTest.php +++ b/tests/Composer/Test/Repository/PlatformRepositoryTest.php @@ -12,65 +12,1213 @@ namespace Composer\Test\Repository; +use Composer\Package\Package; use Composer\Repository\PlatformRepository; use Composer\Test\TestCase; -use Composer\Util\ProcessExecutor; -use Composer\Package\Version\VersionParser; -use Composer\Util\Platform; -use Symfony\Component\Process\ExecutableFinder; +use PHPUnit\Framework\Assert; class PlatformRepositoryTest extends TestCase { - public function testHHVMVersionWhenExecutingInHHVM() + public function testHhvmPackage() { - if (!defined('HHVM_VERSION_ID')) { - $this->markTestSkipped('Not running with HHVM'); - return; - } - $repository = new PlatformRepository(); - $package = $repository->findPackage('hhvm', '*'); - $this->assertNotNull($package, 'failed to find HHVM package'); - $this->assertSame( - sprintf('%d.%d.%d', - HHVM_VERSION_ID / 10000, - (HHVM_VERSION_ID / 100) % 100, - HHVM_VERSION_ID % 100 + $hhvmDetector = $this->getMockBuilder('Composer\Platform\HhvmDetector')->getMock(); + $platformRepository = new PlatformRepository(array(), array(), null, $hhvmDetector); + + $hhvmDetector + ->method('getVersion') + ->willReturn('2.1.0'); + + $hhvm = $platformRepository->findPackage('hhvm', '*'); + self::assertNotNull($hhvm, 'hhvm found'); + + self::assertSame('2.1.0', $hhvm->getPrettyVersion()); + } + + public function getPhpFlavorTestCases() + { + return array( + array( + array( + 'PHP_VERSION' => '7.1.33', + ), + array( + 'php' => '7.1.33' + ) + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + 'PHP_DEBUG' => true, + ), + array( + 'php' => '7.2.31', + 'php-debug' => '7.2.31', + ), + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + 'PHP_ZTS' => true, + ), + array( + 'php' => '7.2.31', + 'php-zts' => '7.2.31', + ), + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + 'PHP_INT_SIZE' => 8, + ), + array( + 'php' => '7.2.31', + 'php-64bit' => '7.2.31', + ), + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + 'AF_INET6' => 30, + ), + array( + 'php' => '7.2.31', + 'php-ipv6' => '7.2.31', + ), + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + ), + array( + 'php' => '7.2.31', + 'php-ipv6' => '7.2.31', + ), + array( + array('inet_pton', array('::'), ''), + ), + ), + array( + array( + 'PHP_VERSION' => '7.2.31-1+ubuntu16.04.1+deb.sury.org+1', + ), + array( + 'php' => '7.2.31', + ), + array( + array('inet_pton', array('::'), false), + ), ), - $package->getPrettyVersion() ); } - public function testHHVMVersionWhenExecutingInPHP() + /** @dataProvider getPhpFlavorTestCases */ + public function testPhpVersion(array $constants, array $packages, array $functions = array()) { - if (defined('HHVM_VERSION_ID')) { - $this->markTestSkipped('Running with HHVM'); - return; - } - if (PHP_VERSION_ID < 50400) { - $this->markTestSkipped('Test only works on PHP 5.4+'); - return; - } - if (Platform::isWindows()) { - $this->markTestSkipped('Test does not run on Windows'); - return; - } - $finder = new ExecutableFinder(); - $hhvm = $finder->find('hhvm'); - if ($hhvm === null) { - $this->markTestSkipped('HHVM is not installed'); - } - $repository = new PlatformRepository(array(), array()); - $package = $repository->findPackage('hhvm', '*'); - $this->assertNotNull($package, 'failed to find HHVM package'); + $runtime = $this->getMockBuilder('Composer\Platform\Runtime')->getMock(); + $runtime + ->method('getExtensions') + ->willReturn(array()); + $runtime + ->method('hasConstant') + ->willReturnMap( + array_map(function($constant) { + return array($constant, null, true); + }, array_keys($constants)) + ); + $runtime + ->method('getConstant') + ->willReturnMap( + array_map(function($constant, $value) { + return array($constant, null, $value); + }, array_keys($constants), array_values($constants)) + ); + $runtime + ->method('invoke') + ->willReturnMap($functions); - $process = new ProcessExecutor(); - $exitCode = $process->execute( - ProcessExecutor::escape($hhvm). - ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', - $version + $repository = new PlatformRepository(array(), array(), $runtime); + foreach ($packages as $packageName => $version) { + $package = $repository->findPackage($packageName, '*'); + self::assertNotNull($package, sprintf('Expected to find package "%s"', $packageName)); + self::assertSame($version, $package->getPrettyVersion(), sprintf('Expected package "%s" version to be %s, got %s', $packageName, $version, $package->getPrettyVersion())); + } + } + + public function testInetPtonRegression() + { + $runtime = $this->getMockBuilder('Composer\Platform\Runtime')->getMock(); + + $runtime + ->expects(self::once()) + ->method('invoke') + ->with('inet_pton', array('::')) + ->willReturn(false); + $runtime + ->method('hasConstant') + ->willReturnMap( + array( + array('PHP_ZTS', false), + array('AF_INET6', false), + ) + ); + $runtime + ->method('getExtensions') + ->willReturn(array()); + $runtime + ->method('getConstant') + ->willReturnMap( + array( + array('PHP_VERSION', null, '7.0.0'), + array('PHP_DEBUG', null, false), + ) + ); + $repository = new PlatformRepository(array(), array(), $runtime); + $package = $repository->findPackage('php-ipv6', '*'); + self::assertNull($package); + } + + public static function getLibraryTestCases() + { + return array( + 'amqp' => array( + 'amqp', + ' + +amqp + +Version => 1.9.4 +Revision => release +Compiled => Nov 19 2019 @ 08:44:26 +AMQP protocol version => 0-9-1 +librabbitmq version => 0.9.0 +Default max channels per connection => 256 +Default max frame size => 131072 +Default heartbeats interval => 0', + array( + 'lib-amqp-protocol' => '0.9.1', + 'lib-amqp-librabbitmq' => '0.9.0', + ) + ), + 'bz2' => array( + 'bz2', + ' +bz2 + +BZip2 Support => Enabled +Stream Wrapper support => compress.bzip2:// +Stream Filter support => bzip2.decompress, bzip2.compress +BZip2 Version => 1.0.5, 6-Sept-2010', + array('lib-bz2' => '1.0.5') + ), + 'curl' => array( + 'curl', + ' +curl + +cURL support => enabled +cURL Information => 7.38.0 +Age => 3 +Features +AsynchDNS => Yes +CharConv => No +Debug => No +GSS-Negotiate => No +IDN => Yes +IPv6 => Yes +krb4 => No +Largefile => Yes +libz => Yes +NTLM => Yes +NTLMWB => Yes +SPNEGO => Yes +SSL => Yes +SSPI => No +TLS-SRP => Yes +HTTP2 => No +GSSAPI => Yes +Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smtp, smtps, telnet, tftp +Host => x86_64-pc-linux-gnu +SSL Version => OpenSSL/1.0.1t +ZLib Version => 1.2.8 +libSSH Version => libssh2/1.4.3 + +Directive => Local Value => Master Value +curl.cainfo => no value => no value', + array( + 'lib-curl' => '2.0.0', + 'lib-curl-openssl' => '1.0.1.20', + 'lib-curl-zlib' => '1.2.8', + 'lib-curl-libssh2' => '1.4.3', + ), + array(array('curl_version', array(), array('version' => '2.0.0'))) + ), + + 'curl: OpenSSL fips version' => array( + 'curl', + ' +curl + +cURL support => enabled +cURL Information => 7.38.0 +Age => 3 +Features +AsynchDNS => Yes +CharConv => No +Debug => No +GSS-Negotiate => No +IDN => Yes +IPv6 => Yes +krb4 => No +Largefile => Yes +libz => Yes +NTLM => Yes +NTLMWB => Yes +SPNEGO => Yes +SSL => Yes +SSPI => No +TLS-SRP => Yes +HTTP2 => No +GSSAPI => Yes +Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smtp, smtps, telnet, tftp +Host => x86_64-pc-linux-gnu +SSL Version => OpenSSL/1.0.1t-fips +ZLib Version => 1.2.8 +libSSH Version => libssh2/1.4.3 + +Directive => Local Value => Master Value +curl.cainfo => no value => no value', + array( + 'lib-curl' => '2.0.0', + 'lib-curl-openssl-fips' => array('1.0.1.20', array(), array('lib-curl-openssl')), + 'lib-curl-zlib' => '1.2.8', + 'lib-curl-libssh2' => '1.4.3', + ), + array(array('curl_version', array(), array('version' => '2.0.0'))) + ), + 'curl: gnutls' => array( + 'curl', + ' +curl + +cURL support => enabled +cURL Information => 7.22.0 +Age => 3 +Features +AsynchDNS => No +CharConv => No +Debug => No +GSS-Negotiate => Yes +IDN => Yes +IPv6 => Yes +krb4 => No +Largefile => Yes +libz => Yes +NTLM => Yes +NTLMWB => Yes +SPNEGO => No +SSL => Yes +SSPI => No +TLS-SRP => Yes +Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, pop3, pop3s, rtmp, rtsp, smtp, smtps, telnet, tftp +Host => x86_64-pc-linux-gnu +SSL Version => GnuTLS/2.12.14 +ZLib Version => 1.2.3.4', + array( + 'lib-curl' => '7.22.0', + 'lib-curl-zlib' => '1.2.3.4', + 'lib-curl-gnutls' => array('2.12.14', array('lib-curl-openssl')), + ), + array(array('curl_version', array(), array('version' => '7.22.0'))) + ), + 'curl: NSS' => array( + 'curl', + ' +curl + +cURL support => enabled +cURL Information => 7.24.0 +Age => 3 +Features +AsynchDNS => Yes +Debug => No +GSS-Negotiate => Yes +IDN => Yes +IPv6 => Yes +Largefile => Yes +NTLM => Yes +SPNEGO => No +SSL => Yes +SSPI => No +krb4 => No +libz => Yes +CharConv => No +Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtsp, scp, sftp, smtp, smtps, telnet, tftp +Host => x86_64-redhat-linux-gnu +SSL Version => NSS/3.13.3.0 +ZLib Version => 1.2.5 +libSSH Version => libssh2/1.4.1', + array( + 'lib-curl' => '7.24.0', + 'lib-curl-nss' => array('3.13.3.0', array('lib-curl-openssl')), + 'lib-curl-zlib' => '1.2.5', + 'lib-curl-libssh2' => '1.4.1', + ), + array(array('curl_version', array(), array('version' => '7.24.0'))) + ), + 'curl: libssh not libssh2' => array( + 'curl', + ' + +curl + +cURL support => enabled +cURL Information => 7.68.0 +Age => 5 +Features +AsynchDNS => Yes +CharConv => No +Debug => No +GSS-Negotiate => No +IDN => Yes +IPv6 => Yes +krb4 => No +Largefile => Yes +libz => Yes +NTLM => Yes +NTLMWB => Yes +SPNEGO => Yes +SSL => Yes +SSPI => No +TLS-SRP => Yes +HTTP2 => Yes +GSSAPI => Yes +KERBEROS5 => Yes +UNIX_SOCKETS => Yes +PSL => Yes +HTTPS_PROXY => Yes +MULTI_SSL => No +BROTLI => Yes +Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smb, smbs, smtp, smtps, telnet, tftp +Host => x86_64-pc-linux-gnu +SSL Version => OpenSSL/1.1.1g +ZLib Version => 1.2.11 +libSSH Version => libssh/0.9.3/openssl/zlib', + array( + 'lib-curl' => '7.68.0', + 'lib-curl-openssl' => '1.1.1.7', + 'lib-curl-zlib' => '1.2.11', + 'lib-curl-libssh' => '0.9.3', + ), + array(array('curl_version', array(), array('version' => '7.68.0'))), + ), + 'date' => array( + 'date', + ' +date + +date/time support => enabled +timelib version => 2018.03 +"Olson" Timezone Database Version => 2020.1 +Timezone Database => external +Default timezone => Europe/Berlin', + array( + 'lib-date-timelib' => '2018.03', + 'lib-date-zoneinfo' => '2020.1', + ) + ), + 'date: before timelib was extracted' => array( + 'date', + ' +date + +date/time support => enabled +"Olson" Timezone Database Version => 2013.2 +Timezone Database => internal +Default timezone => Europe/Amsterdam', + array( + 'lib-date-zoneinfo' => '2013.2', + 'lib-date-timelib' => false, + ) + ), + 'date: internal zoneinfo' => array( + array('date', 'timezonedb'), + ' +date + +date/time support => enabled +"Olson" Timezone Database Version => 2020.1 +Timezone Database => internal +Default timezone => UTC', + array('lib-date-zoneinfo' => '2020.1') + ), + 'date: external zoneinfo' => array( + array('date', 'timezonedb'), + ' +date + +date/time support => enabled +"Olson" Timezone Database Version => 2020.1 +Timezone Database => external +Default timezone => UTC', + array('lib-timezonedb-zoneinfo' => array('2020.1', array('lib-date-zoneinfo'))) + ), + 'date: zoneinfo 0.system' => array( + 'date', + ' + + +date/time support => enabled +timelib version => 2018.03 +"Olson" Timezone Database Version => 0.system +Timezone Database => internal +Default timezone => Europe/Berlin + +Directive => Local Value => Master Value +date.timezone => no value => no value +date.default_latitude => 31.7667 => 31.7667 +date.default_longitude => 35.2333 => 35.2333 +date.sunset_zenith => 90.583333 => 90.583333 +date.sunrise_zenith => 90.583333 => 90.583333', + array( + 'lib-date-zoneinfo' => '0', + 'lib-date-timelib' => '2018.03', + ) + ), + 'fileinfo' => array( + 'fileinfo', + ' +fileinfo + +fileinfo support => enabled +libmagic => 537', + array('lib-fileinfo-libmagic' => '537') + ), + 'gd' => array( + 'gd', + ' +gd + +GD Support => enabled +GD Version => bundled (2.1.0 compatible) +FreeType Support => enabled +FreeType Linkage => with freetype +FreeType Version => 2.10.0 +GIF Read Support => enabled +GIF Create Support => enabled +JPEG Support => enabled +libJPEG Version => 9 compatible +PNG Support => enabled +libPNG Version => 1.6.34 +WBMP Support => enabled +XBM Support => enabled +WebP Support => enabled + +Directive => Local Value => Master Value +gd.jpeg_ignore_warning => 1 => 1', + array( + 'lib-gd' => '1.2.3', + 'lib-gd-freetype' => '2.10.0', + 'lib-gd-libjpeg' => '9.0', + 'lib-gd-libpng' => '1.6.34', + ), + array(), + array(array('GD_VERSION', null, '1.2.3')) + ), + 'gd: libjpeg version variation' => array( + 'gd', + ' +gd + +GD Support => enabled +GD Version => bundled (2.1.0 compatible) +FreeType Support => enabled +FreeType Linkage => with freetype +FreeType Version => 2.9.1 +GIF Read Support => enabled +GIF Create Support => enabled +JPEG Support => enabled +libJPEG Version => 6b +PNG Support => enabled +libPNG Version => 1.6.35 +WBMP Support => enabled +XBM Support => enabled +WebP Support => enabled + +Directive => Local Value => Master Value +gd.jpeg_ignore_warning => 1 => 1', + array( + 'lib-gd' => '1.2.3', + 'lib-gd-freetype' => '2.9.1', + 'lib-gd-libjpeg' => '6.2', + 'lib-gd-libpng' => '1.6.35', + ), + array(), + array(array('GD_VERSION', null, '1.2.3')) + ), + 'gd: libxpm' => array( + 'gd', + ' +gd + +GD Support => enabled +GD headers Version => 2.2.5 +GD library Version => 2.2.5 +FreeType Support => enabled +FreeType Linkage => with freetype +FreeType Version => 2.6.3 +GIF Read Support => enabled +GIF Create Support => enabled +JPEG Support => enabled +libJPEG Version => 6b +PNG Support => enabled +libPNG Version => 1.6.28 +WBMP Support => enabled +XPM Support => enabled +libXpm Version => 30411 +XBM Support => enabled +WebP Support => enabled + +Directive => Local Value => Master Value +gd.jpeg_ignore_warning => 1 => 1', + array( + 'lib-gd' => '2.2.5', + 'lib-gd-freetype' => '2.6.3', + 'lib-gd-libjpeg' => '6.2', + 'lib-gd-libpng' => '1.6.28', + 'lib-gd-libxpm' => '3.4.11', + ), + array(), + array(array('GD_VERSION', null, '2.2.5')) + ), + 'iconv' => array( + 'iconv', + null, + array('lib-iconv' => '1.2.4'), + array(), + array(array('ICONV_VERSION', null, '1.2.4')) + ), + 'gmp' => array( + 'gmp', + null, + array('lib-gmp' => '6.1.0'), + array(), + array(array('GMP_VERSION', null, '6.1.0')) + ), + 'intl' => array( + 'intl', + ' +intl + +Internationalization support => enabled +ICU version => 57.1 +ICU Data version => 57.1 +ICU TZData version => 2016b +ICU Unicode version => 8.0 + +Directive => Local Value => Master Value +intl.default_locale => no value => no value +intl.error_level => 0 => 0 +intl.use_exceptions => 0 => 0', + array( + 'lib-icu' => '100', + 'lib-icu-cldr' => ResourceBundleStub::STUB_VERSION, + 'lib-icu-unicode' => '7.0.0', + 'lib-icu-zoneinfo' => '2016.2', + ), + array( + array(array('ResourceBundle', 'create'), array('root', 'ICUDATA', false), new ResourceBundleStub()), + array(array('IntlChar', 'getUnicodeVersion'), array(), array(7, 0, 0, 0)), + ), + array(array('INTL_ICU_VERSION', null, '100')), + array( + array('ResourceBundle'), + array('IntlChar'), + ) + ), + 'intl: INTL_ICU_VERSION not defined' => array( + 'intl', + ' +intl + +Internationalization support => enabled +version => 1.1.0 +ICU version => 57.1 +ICU Data version => 57.1', + array('lib-icu' => '57.1'), + ), + 'imagick: 6.x' => array( + 'imagick', + null, + array('lib-imagick-imagemagick' => array('6.2.9', array('lib-imagick'))), + array(), + array(), + array(array('Imagick', array(), new ImagickStub('ImageMagick 6.2.9 Q16 x86_64 2018-05-18 http://www.imagemagick.org'))) + ), + 'imagick: 7.x' => array( + 'imagick', + null, + array('lib-imagick-imagemagick' => array('7.0.8.34', array('lib-imagick'))), + array(), + array(), + array(array('Imagick', array(), new ImagickStub('ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org'))) + ), + 'ldap' => array( + 'ldap', + ' +ldap + +LDAP Support => enabled +RCS Version => $Id: 5f1913de8e05a346da913956f81e0c0d8991c7cb $ +Total Links => 0/unlimited +API Version => 3001 +Vendor Name => OpenLDAP +Vendor Version => 20450 +SASL Support => Enabled + +Directive => Local Value => Master Value +ldap.max_links => Unlimited => Unlimited', + array('lib-ldap-openldap' => '2.4.50') + ), + 'libxml' => array( + 'libxml', + null, + array('lib-libxml' => '2.1.5'), + array(), + array(array('LIBXML_DOTTED_VERSION', null, '2.1.5')) + ), + 'libxml: related extensions' => array( + array('libxml', 'dom', 'simplexml', 'xml', 'xmlreader', 'xmlwriter'), + null, + array('lib-libxml' => array('2.1.5', array(), array('lib-dom-libxml', 'lib-simplexml-libxml', 'lib-xml-libxml', 'lib-xmlreader-libxml', 'lib-xmlwriter-libxml'))), + array(), + array(array('LIBXML_DOTTED_VERSION', null, '2.1.5')) + ), + 'mbstring' => array( + 'mbstring', + ' +mbstring + +Multibyte Support => enabled +Multibyte string engine => libmbfl +HTTP input encoding translation => disabled +libmbfl version => 1.3.2 + +mbstring extension makes use of "streamable kanji code filter and converter", which is distributed under the GNU Lesser General Public License version 2.1. + +Multibyte (japanese) regex support => enabled +Multibyte regex (oniguruma) version => 6.1.3', + array( + 'lib-mbstring-libmbfl' => '1.3.2', + 'lib-mbstring-oniguruma' => '7.0.0', + ), + array(), + array(array('MB_ONIGURUMA_VERSION', null, '7.0.0')) + ), + 'mbstring: no MB_ONIGURUMA constant' => array( + 'mbstring', + ' +mbstring + +Multibyte Support => enabled +Multibyte string engine => libmbfl +HTTP input encoding translation => disabled +libmbfl version => 1.3.2 + +mbstring extension makes use of "streamable kanji code filter and converter", which is distributed under the GNU Lesser General Public License version 2.1. + +Multibyte (japanese) regex support => enabled +Multibyte regex (oniguruma) version => 6.1.3', + array( + 'lib-mbstring-libmbfl' => '1.3.2', + 'lib-mbstring-oniguruma' => '6.1.3', + ) + ), + 'mbstring: no MB_ONIGURUMA constant <7.40' => array( + 'mbstring', + ' +mbstring + +Multibyte Support => enabled +Multibyte string engine => libmbfl +HTTP input encoding translation => disabled +libmbfl version => 1.3.2 +oniguruma version => 6.9.4 + +mbstring extension makes use of "streamable kanji code filter and converter", which is distributed under the GNU Lesser General Public License version 2.1. + +Multibyte (japanese) regex support => enabled +Multibyte regex (oniguruma) backtrack check => On', + array( + 'lib-mbstring-libmbfl' => '1.3.2', + 'lib-mbstring-oniguruma' => '6.9.4', + ), + ), + 'memcached' => array( + 'memcached', + ' +memcached + +memcached support => enabled +Version => 3.1.5 +libmemcached version => 1.0.18 +SASL support => yes +Session support => yes +igbinary support => yes +json support => yes +msgpack support => yes', + array('lib-memcached-libmemcached' => '1.0.18') + ), + 'openssl' => array( + 'openssl', + null, + array('lib-openssl' => '1.1.1.7'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g 21 Apr 2020')) + ), + 'openssl: two letters suffix' => array( + 'openssl', + null, + array('lib-openssl' => '0.9.8.33'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 0.9.8zg 21 Apr 2020')) + ), + 'openssl: pre release is treated as alpha' => array( + 'openssl', + null, + array('lib-openssl' => '1.1.1.7-alpha1'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g-pre1 21 Apr 2020')) + ), + 'openssl: beta release' => array( + 'openssl', + null, + array('lib-openssl' => '1.1.1.7-beta2'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g-beta2 21 Apr 2020')) + ), + 'openssl: alpha release' => array( + 'openssl', + null, + array('lib-openssl' => '1.1.1.7-alpha4'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g-alpha4 21 Apr 2020')) + ), + 'openssl: rc release' => array( + 'openssl', + null, + array('lib-openssl' => '1.1.1.7-rc2'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g-rc2 21 Apr 2020')) + ), + 'openssl: fips' => array( + 'openssl', + null, + array('lib-openssl-fips' => array('1.1.1.7', array(), array('lib-openssl'))), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'OpenSSL 1.1.1g-fips 21 Apr 2020')) + ), + 'openssl: LibreSSL' => array( + 'openssl', + null, + array('lib-openssl' => '2.0.1.0'), + array(), + array(array('OPENSSL_VERSION_TEXT', null, 'LibreSSL 2.0.1')) + ), + 'mysqlnd' => array( + 'mysqlnd', + ' + mysqlnd + +mysqlnd => enabled +Version => mysqlnd 5.0.11-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $ +Compression => supported +core SSL => supported +extended SSL => supported +Command buffer size => 4096 +Read buffer size => 32768 +Read timeout => 31536000 +Collecting statistics => Yes +Collecting memory statistics => Yes +Tracing => n/a +Loaded plugins => mysqlnd,debug_trace,auth_plugin_mysql_native_password,auth_plugin_mysql_clear_password,auth_plugin_sha256_password +API Extensions => pdo_mysql,mysqli', + array('lib-mysqlnd-mysqlnd' => '5.0.11-dev') + ), + 'pdo_mysql' => array( + 'pdo_mysql', + ' + pdo_mysql + +PDO Driver for MySQL => enabled +Client API version => mysqlnd 5.0.10-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $ + +Directive => Local Value => Master Value +pdo_mysql.default_socket => /tmp/mysql.sock => /tmp/mysql.sock', + array('lib-pdo_mysql-mysqlnd' => '5.0.10-dev') + ), + 'mongodb' => array( + 'mongodb', + ' + mongodb + +MongoDB support => enabled +MongoDB extension version => 1.6.1 +MongoDB extension stability => stable +libbson bundled version => 1.15.2 +libmongoc bundled version => 1.15.2 +libmongoc SSL => enabled +libmongoc SSL library => OpenSSL +libmongoc crypto => enabled +libmongoc crypto library => libcrypto +libmongoc crypto system profile => disabled +libmongoc SASL => disabled +libmongoc ICU => enabled +libmongoc compression => enabled +libmongoc compression snappy => disabled +libmongoc compression zlib => enabled + +Directive => Local Value => Master Value +mongodb.debug => no value => no value', + array( + 'lib-mongodb-libmongoc' => '1.15.2', + 'lib-mongodb-libbson' => '1.15.2', + ) + ), + 'pcre' => array( + 'pcre', + ' +pcre + +PCRE (Perl Compatible Regular Expressions) Support => enabled +PCRE Library Version => 10.33 2019-04-16 +PCRE Unicode Version => 11.0.0 +PCRE JIT Support => enabled +PCRE JIT Target => x86 64bit (little endian + unaligned)', + array( + 'lib-pcre' => '10.33', + 'lib-pcre-unicode' => '11.0.0', + ), + array(), + array(array('PCRE_VERSION', null, '10.33 2019-04-16')) + ), + 'pcre: no unicode version included' => array( + 'pcre', + ' +pcre + +PCRE (Perl Compatible Regular Expressions) Support => enabled +PCRE Library Version => 8.38 2015-11-23 + +Directive => Local Value => Master Value +pcre.backtrack_limit => 1000000 => 1000000 +pcre.recursion_limit => 100000 => 100000 + ', + array( + 'lib-pcre' => '8.38', + ), + array(), + array(array('PCRE_VERSION', null, '8.38 2015-11-23')) + ), + 'pgsql' => array( + 'pgsql', + ' +pgsql + +PostgreSQL Support => enabled +PostgreSQL(libpq) Version => 12.2 +PostgreSQL(libpq) => PostgreSQL 12.3 on x86_64-apple-darwin18.7.0, compiled by Apple clang version 11.0.0 (clang-1100.0.33.17), 64-bit +Multibyte character support => enabled +SSL support => enabled +Active Persistent Links => 0 +Active Links => 0 + +Directive => Local Value => Master Value +pgsql.allow_persistent => On => On +pgsql.max_persistent => Unlimited => Unlimited +pgsql.max_links => Unlimited => Unlimited +pgsql.auto_reset_persistent => Off => Off +pgsql.ignore_notice => Off => Off +pgsql.log_notice => Off => Off', + array('lib-pgsql-libpq' => '12.2') + ), + 'pdo_pgsql' => array( + 'pdo_pgsql', + ' + pdo_pgsql + +PDO Driver for PostgreSQL => enabled +PostgreSQL(libpq) Version => 12.1 +Module version => 7.1.33 +Revision => $Id: 9c5f356c77143981d2e905e276e439501fe0f419 $', + array('lib-pdo_pgsql-libpq' => '12.1') + ), + 'libsodium' => array( + 'libsodium', + null, + array('lib-libsodium' => '1.0.17'), + array(), + array(array('SODIUM_LIBRARY_VERSION', null, '1.0.17')) + ), + 'libsodium: different extension name' => array( + 'sodium', + null, + array('lib-libsodium' => '1.0.15'), + array(), + array(array('SODIUM_LIBRARY_VERSION', null, '1.0.15')) + ), + 'pdo_sqlite' => array( + 'pdo_sqlite', + ' +pdo_sqlite + +PDO Driver for SQLite 3.x => enabled +SQLite Library => 3.32.3 + ', + array('lib-pdo_sqlite-sqlite' => '3.32.3') + ), + 'sqlite3' => array( + 'sqlite3', + ' +sqlite3 + +SQLite3 support => enabled +SQLite3 module version => 7.1.33 +SQLite Library => 3.31.0 + +Directive => Local Value => Master Value +sqlite3.extension_dir => no value => no value +sqlite3.defensive => 1 => 1', + array('lib-sqlite3-sqlite' => '3.31.0') + ), + 'ssh2' => array( + 'ssh2', + ' +ssh2 + +SSH2 support => enabled +extension version => 1.2 +libssh2 version => 1.8.0 +banner => SSH-2.0-libssh2_1.8.0', + array('lib-ssh2-libssh2' => '1.8.0') + ), + 'yaml' => array( + 'yaml', + ' + yaml + +LibYAML Support => enabled +Module Version => 2.0.2 +LibYAML Version => 0.2.2 + +Directive => Local Value => Master Value +yaml.decode_binary => 0 => 0 +yaml.decode_timestamp => 0 => 0 +yaml.decode_php => 0 => 0 +yaml.output_canonical => 0 => 0 +yaml.output_indent => 2 => 2 +yaml.output_width => 80 => 80', + array('lib-yaml-libyaml' => '0.2.2') + ), + 'xsl' => array( + 'xsl', + ' +xsl + +XSL => enabled +libxslt Version => 1.1.33 +libxslt compiled against libxml Version => 2.9.8 +EXSLT => enabled +libexslt Version => 1.1.29', + array( + 'lib-libxslt' => array('1.1.29', array('lib-xsl')), + 'lib-libxslt-libxml' => '2.9.8', + ), + array(), + array(array('LIBXSLT_DOTTED_VERSION', null, '1.1.29')) + ), + 'zip' => array( + 'zip', + null, + array('lib-zip-libzip' => array('1.5.0', array('lib-zip'))), + array(), + array(array('LIBZIP_VERSION', 'ZipArchive', '1.5.0')), + ), + 'zlib' => array( + 'zlib', + null, + array('lib-zlib' => '1.2.10'), + array(), + array(array('ZLIB_VERSION', null, '1.2.10')), + ), + 'zlib: no constant present' => array( + 'zlib', + ' +zlib + +ZLib Support => enabled +Stream Wrapper => compress.zlib:// +Stream Filter => zlib.inflate, zlib.deflate +Compiled Version => 1.2.8 +Linked Version => 1.2.11', + array('lib-zlib' => '1.2.11'), + ), ); - $parser = new VersionParser; + } - $this->assertSame($parser->normalize($version), $package->getVersion()); + /** + * @dataProvider getLibraryTestCases + * + * @param string|string[] $extensions + * @param string|null $info + * @param array $expectations + * @param array $functions + * @param array $constants + * @param array $classes + */ + public function testLibraryInformation( + $extensions, + $info, + array $expectations, + array $functions = array(), + array $constants = array(), + array $classDefinitions = array() + ) + { + $extensions = (array)$extensions; + + $extensionVersion = '100.200.300'; + + $runtime = $this->getMockBuilder('Composer\Platform\Runtime')->getMock(); + $runtime + ->method('getExtensions') + ->willReturn($extensions); + + + $runtime + ->method('getExtensionVersion') + ->willReturnMap( + array_map(function($extension) use ($extensionVersion) { + return array($extension, $extensionVersion); + }, $extensions) + ); + + $runtime + ->method('getExtensionInfo') + ->willReturnMap( + array_map(function ($extension) use ($info) { + return array($extension, $info); + }, $extensions) + ); + + $runtime + ->method('invoke') + ->willReturnMap($functions); + + $constants[] = array('PHP_VERSION', null, '7.1.0'); + $runtime + ->method('hasConstant') + ->willReturnMap( + array_map( + function ($constantDefintion) { return array($constantDefintion[0], $constantDefintion[1], true); }, + $constants + ) + ); + $runtime + ->method('getConstant') + ->willReturnMap($constants); + + $runtime + ->method('hasClass') + ->willReturnMap( + array_map( + function ($classDefinition) { return array($classDefinition[0], true); }, + $classDefinitions + ) + ); + $runtime + ->method('construct') + ->willReturnMap($classDefinitions); + + $platformRepository = new PlatformRepository(array(), array(), $runtime); + + $expectations = array_map(function ($expectation) { + return array_replace(array(null, array(), array()), (array) $expectation); + }, $expectations); + + $libraries = array_map( + function ($package) { + return $package['name']; + }, array_filter( + $platformRepository->search('lib', PlatformRepository::SEARCH_NAME), + function ($package) { + return strpos($package['name'], 'lib-') === 0; + } + ) + ); + $expectedLibraries = array_merge(array_keys(array_filter($expectations,function($expectation) { return $expectation[0] !== false; }))); + self::assertCount(count(array_filter($expectedLibraries)), $libraries, sprintf('Expected: %s, got %s', var_export($expectedLibraries, true), var_export($libraries, true))); + + $expectations = array_merge($expectations, array_combine(array_map(function($extension) { + return 'ext-'.$extension; + }, $extensions), array_fill(0, count($extensions), array($extensionVersion, array(), array())))); + + foreach ($expectations as $packageName => $expectation) { + list($expectedVersion, $expectedReplaces, $expectedProvides) = $expectation; + + $package = $platformRepository->findPackage($packageName, '*'); + if ($expectedVersion === false) { + self::assertNull($package, sprintf('Expected to not find package "%s"', $packageName)); + } else { + self::assertNotNull($package, sprintf('Expected to find package "%s"', $packageName)); + self::assertSame($expectedVersion, $package->getPrettyVersion(), sprintf('Expected version %s for %s', $expectedVersion, $packageName)); + $this->assertPackageLinks('replaces', $expectedReplaces, $package, $package->getReplaces()); + $this->assertPackageLinks('provides', $expectedProvides, $package, $package->getProvides()); + } + } + } + + private function assertPackageLinks($context, array $expectedLinks, Package $sourcePackage, array $links) + { + self::assertCount(count($expectedLinks), $links, sprintf('%s: expected package count to match', $context)); + + foreach ($links as $link) { + self::assertSame($sourcePackage->getName(), $link->getSource()); + self::assertContains($link->getTarget(), $expectedLinks, sprintf('%s: package %s not in %s', $context, $link->getTarget(), var_export($expectedLinks, true))); + self::assertTrue($link->getConstraint()->matches($this->getVersionConstraint('=', $sourcePackage->getVersion()))); + } + } +} + +class ResourceBundleStub { + const STUB_VERSION = '32.0.1'; + + public static function create($locale, $bundleName, $fallback) { + Assert::assertSame(3, func_num_args()); + Assert::assertSame('root', $locale); + Assert::assertSame('ICUDATA', $bundleName); + Assert::assertFalse($fallback); + + return new self(); + } + + public function get($field) { + Assert::assertSame(1, func_num_args()); + Assert::assertSame('Version', $field); + + return self::STUB_VERSION; + } +} + +class ImagickStub { + private $versionString; + + public function __construct($versionString) { + $this->versionString = $versionString; + } + + public function getVersion() { + Assert::assertSame(0, func_num_args()); + + return array('versionString' => $this->versionString); } } diff --git a/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz new file mode 100644 index 000000000..805860f87 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz new file mode 100644 index 000000000..205472426 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz new file mode 100644 index 000000000..179a5bd97 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz new file mode 100644 index 000000000..d6a64264d Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz new file mode 100644 index 000000000..b2e272e90 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz new file mode 100644 index 000000000..8fb347604 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip b/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip index db8c50302..96c0959bb 100644 Binary files a/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip and b/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/multiple_subfolders.zip b/tests/Composer/Test/Util/Fixtures/Zip/multiple_subfolders.zip new file mode 100644 index 000000000..5c5bc5adf Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/multiple_subfolders.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/single-sub.zip b/tests/Composer/Test/Util/Fixtures/Zip/single-sub.zip new file mode 100644 index 000000000..b8ccd4c76 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/single-sub.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip b/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip deleted file mode 100644 index 93060bea2..000000000 Binary files a/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip and /dev/null differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/subfolders.zip b/tests/Composer/Test/Util/Fixtures/Zip/subfolders.zip new file mode 100644 index 000000000..06827d6a4 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/subfolders.zip differ diff --git a/tests/Composer/Test/Util/TarTest.php b/tests/Composer/Test/Util/TarTest.php new file mode 100644 index 000000000..b14d0c0f9 --- /dev/null +++ b/tests/Composer/Test/Util/TarTest.php @@ -0,0 +1,71 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\Tar; +use Composer\Test\TestCase; + +/** + * @author Wissem Riahi + */ +class TarTest extends TestCase +{ + public function testReturnsNullifTheTarIsNotFound() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/invalid.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheTarIsEmpty() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/empty.tar.gz'); + $this->assertNull($result); + } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheTarHasNoComposerJson() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/nojson.tar.gz'); + } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheComposerJsonIsInASubSubfolder() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/subfolders.tar.gz'); + } + + public function testReturnsComposerJsonInTarRoot() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/root.tar.gz'); + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsComposerJsonInFirstFolder() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/folder.tar.gz'); + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + /** + * @expectedException \RuntimeException + */ + public function testMultipleTopLevelDirsIsInvalid() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/multiple.tar.gz'); + } +} diff --git a/tests/Composer/Test/Util/ZipTest.php b/tests/Composer/Test/Util/ZipTest.php index 19dc54fa1..ba61e8bf6 100644 --- a/tests/Composer/Test/Util/ZipTest.php +++ b/tests/Composer/Test/Util/ZipTest.php @@ -20,7 +20,7 @@ use Composer\Test\TestCase; */ class ZipTest extends TestCase { - public function testThrowsExceptionIfZipExcentionIsNotLoaded() + public function testThrowsExceptionIfZipExtensionIsNotLoaded() { if (extension_loaded('zip')) { $this->markTestSkipped('The PHP zip extension is loaded.'); @@ -55,28 +55,30 @@ class ZipTest extends TestCase $this->assertNull($result); } - public function testReturnsNullIfTheZipHasNoComposerJson() + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheZipHasNoComposerJson() { if (!extension_loaded('zip')) { $this->markTestSkipped('The PHP zip extension is not loaded.'); return; } - $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip'); - - $this->assertNull($result); + Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip'); } - public function testReturnsNullIfTheComposerJsonIsInASubSubfolder() + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheComposerJsonIsInASubSubfolder() { if (!extension_loaded('zip')) { $this->markTestSkipped('The PHP zip extension is not loaded.'); return; } - $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip'); - - $this->assertNull($result); + Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolders.zip'); } public function testReturnsComposerJsonInZipRoot() @@ -99,19 +101,44 @@ class ZipTest extends TestCase } $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } - public function testReturnsRootComposerJsonAndSkipsSubfolders() + /** + * @expectedException \RuntimeException + */ + public function testMultipleTopLevelDirsIsInvalid() { if (!extension_loaded('zip')) { $this->markTestSkipped('The PHP zip extension is not loaded.'); return; } - $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip'); + Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip'); + } + + public function testReturnsComposerJsonFromFirstSubfolder() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/single-sub.zip'); $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfMultipleComposerInSubFoldersWereFound() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple_subfolders.zip'); + } }