Skip to content
Snippets Groups Projects
Commit 346b9805 authored by Travis Carden's avatar Travis Carden Committed by Adam G-H
Browse files

Issue #3312779 by TravisCarden, phenaproxima: Improve Composer package name validation

parent 4d23ebde
No related branches found
No related tags found
No related merge requests found
......@@ -5,7 +5,9 @@ namespace Drupal\package_manager;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Package\Loader\ValidatingArrayLoader;
use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Comparator;
use Drupal\Component\Serialization\Yaml;
......@@ -264,6 +266,52 @@ class ComposerUtility {
return NULL;
}
/**
* Determines whether a Composer requirement string is valid.
*
* @param string $requirement
* A requirement string, optionally with a version constraint, e.g.,
* "vendor/package" or "vendor/package:1.2.3", or any combination of package
* name and requirement string that Composer understands.
*
* @return bool
* TRUE if the requirement string is valid, FALSE otherwise.
*
* @see https://getcomposer.org/doc/04-schema.md#name
* @see https://getcomposer.org/doc/articles/versions.md
*
* @internal
* This method may be changed or removed at any time without warning and
* should not be used by external code.
*/
public static function isValidRequirement(string $requirement): bool {
$version_parser = new VersionParser();
$parts = $version_parser->parseNameVersionPairs([$requirement])[0];
$package_name = $parts['name'];
$version = $parts['version'] ?? NULL;
// Validate just the package name.
if (ValidatingArrayLoader::hasPackageNamingError($package_name)) {
return FALSE;
}
// Return early if there's no version constraint to validate.
if ($version === NULL) {
return TRUE;
}
// Validate the version constraint.
try {
$version_parser->parseConstraints($version);
}
catch (\UnexpectedValueException $e) {
return FALSE;
}
// All good.
return TRUE;
}
/**
* Scans a given path to determine the Drupal project name.
*
......
......@@ -725,14 +725,7 @@ class Stage implements LoggerAwareInterface {
*/
protected function validatePackageNames(array $package_versions): void {
foreach ($package_versions as $package_name) {
$package_name = trim($package_name);
// Don't mistake the legitimate `php` and `composer` platform requirements
// for Drupal projects.
if ($package_name === 'php' || $package_name === 'composer') {
continue;
}
elseif (preg_match('/^[a-z0-9_]+$/i', $package_name)) {
if (!ComposerUtility::isValidRequirement($package_name)) {
throw new \InvalidArgumentException("Invalid package name '$package_name'.");
}
}
......
......@@ -469,26 +469,8 @@ class StageTest extends PackageManagerKernelTestBase {
*/
public function providerValidatePackageNames(): array {
return [
// White space is trimmed out of package names during validation.
'empty string' => ['', FALSE],
'white space' => [' ', FALSE],
// The `composer` and `php` requirements are special, since they could
// otherwise be mistaken for Drupal project names.
'Composer runtime, unconstrained' => ['composer', FALSE],
'Composer runtime, constrained' => ['composer:^2.4', FALSE],
'Composer runtime API, unconstrained' => ['composer-runtime-api', FALSE],
'Composer runtime API, constrained' => ['composer-runtime-api:~2.2.0', FALSE],
'PHP runtime, unconstrained' => ['php', FALSE],
'PHP runtime, constrained' => ['php:>=7.4', FALSE],
'PHP runtime variant, unconstrained' => ['php-zts', FALSE],
'PHP runtime variant, constrained' => ['php-zts:8.1', FALSE],
'PHP extension, unconstrained' => ['ext-json', FALSE],
'PHP extension, constrained' => ['ext-json:7.4.1', FALSE],
'PHP library, unconstrained' => ['lib-curl', FALSE],
'PHP library, constrained' => ['lib-curl:^7', FALSE],
'Drupal package, unconstrained' => ['drupal/semver_test', FALSE],
'Drupal package, constrained' => ['drupal/semver_test:^1.10', FALSE],
'Drupal project' => ['semver_test', TRUE],
'Full package name' => ['drupal/semver_test', FALSE],
'Bare Drupal project name' => ['semver_test', TRUE],
];
}
......
......@@ -52,6 +52,100 @@ class ComposerUtilityTest extends UnitTestCase {
$this->assertSame($expected_core_package_names, array_keys($core_packages));
}
/**
* @covers ::isValidRequirement
*
* @param bool $expected_is_valid
* Whether the given requirement string is valid.
* @param string $requirement
* The requirement string to validate.
*
* @dataProvider providerIsValidRequirement
*/
public function testIsValidRequirement(bool $expected_is_valid, string $requirement): void {
$this->assertSame($expected_is_valid, ComposerUtility::isValidRequirement($requirement));
}
/**
* Data provider for ::testIsValidRequirement().
*
* @return \string[][][]
* The test cases.
*/
public function providerIsValidRequirement(): array {
return [
// Valid requirements.
[TRUE, 'vendor/package'],
[TRUE, 'vendor/snake_case'],
[TRUE, 'vendor/kebab-case'],
[TRUE, 'vendor/with.dots'],
[TRUE, '1vendor2/3package4'],
[TRUE, 'vendor/package:1'],
[TRUE, 'vendor/package:1.2'],
[TRUE, 'vendor/package:1.2.3'],
[TRUE, 'vendor/package:1.x'],
[TRUE, 'vendor/package:^1'],
[TRUE, 'vendor/package:~1'],
[TRUE, 'vendor/package:>1'],
[TRUE, 'vendor/package:<1'],
[TRUE, 'vendor/package:>=1'],
[TRUE, 'vendor/package:>1 <2'],
[TRUE, 'vendor/package:1 || 2'],
[TRUE, 'vendor/package:>=1,<1.1.0'],
[TRUE, 'vendor/package:1a'],
[TRUE, 'vendor/package:*'],
[TRUE, 'vendor/package:dev-master'],
[TRUE, 'vendor/package:*@dev'],
[TRUE, 'vendor/package:@dev'],
[TRUE, 'vendor/package:master@dev'],
[TRUE, 'vendor/package:master@beta'],
[TRUE, 'php'],
[TRUE, 'php:8'],
[TRUE, 'php:8.0'],
[TRUE, 'php:^8.1'],
[TRUE, 'php:~8.1'],
[TRUE, 'php-64bit'],
[TRUE, 'composer'],
[TRUE, 'composer-plugin-api'],
[TRUE, 'composer-plugin-api:1'],
[TRUE, 'ext-json'],
[TRUE, 'ext-json:1'],
[TRUE, 'ext-pdo_mysql'],
[TRUE, 'ext-pdo_mysql:1'],
[TRUE, 'lib-curl'],
[TRUE, 'lib-curl:1'],
[TRUE, 'lib-curl-zlib'],
[TRUE, 'lib-curl-zlib:1'],
// Invalid requirements.
[FALSE, ''],
[FALSE, ' '],
[FALSE, '/'],
[FALSE, 'php8'],
[FALSE, 'package'],
[FALSE, 'vendor\package'],
[FALSE, 'vendor//package'],
[FALSE, 'vendor/package1 vendor/package2'],
[FALSE, 'vendor/package/extra'],
[FALSE, 'vendor/package:a'],
[FALSE, 'vendor/package:'],
[FALSE, 'vendor/package::'],
[FALSE, 'vendor/package::1'],
[FALSE, 'vendor/package:1:2'],
[FALSE, 'vendor/package:develop@dev@dev'],
[FALSE, 'vendor/package:develop@'],
[FALSE, 'vEnDor/pAcKaGe'],
[FALSE, '_vendor/package'],
[FALSE, '_vendor/_package'],
[FALSE, 'vendor_/package'],
[FALSE, '_vendor/package_'],
[FALSE, 'vendor/package-'],
[FALSE, 'php-'],
[FALSE, 'ext'],
[FALSE, 'lib'],
];
}
/**
* @covers ::getPackagesNotIn
* @covers ::getPackagesWithDifferentVersionsIn
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment