diff --git a/dictionary.txt b/dictionary.txt index 9f150417896ba8dcfde10aaa0b16eff4ad6cfdd7..d2451cc3a3f39e3299bd2b97ab2be5d997a97830 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -9,3 +9,4 @@ testlogger unwritable filedate unshallow +hhvm diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php index 9a6d5d7f1850b6cad5373b50faa4563916542df7..8d1fb28afbf705f6ef50ed8bb28ad97782826f2f 100644 --- a/package_manager/src/Stage.php +++ b/package_manager/src/Stage.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace Drupal\package_manager; +use Composer\Semver\VersionParser; use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Utility\Crypt; use Drupal\Core\File\Exception\FileException; @@ -121,6 +122,29 @@ class Stage implements LoggerAwareInterface { */ private const TEMPSTORE_DESTROYED_STAGES_INFO_PREFIX = 'TEMPSTORE_DESTROYED_STAGES_INFO'; + /** + * The regular expression to check if a package name is a platform package. + * + * @var string + * + * @see \Composer\Repository\PlatformRepository::PLATFORM_PACKAGE_REGEX + * @see ::validateRequirements() + */ + private const COMPOSER_PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer(?:-(?:plugin|runtime)-api)?)$}iD'; + + /** + * The regular expression to check if a package name is a regular package. + * + * If you try to require an invalid package name, this is the regular + * expression that Composer will, at the command line, tell you to match. + * + * @var string + * + * @see \Composer\Package\Loader\ValidatingArrayLoader::hasPackageNamingError() + * @see ::validateRequirements() + */ + private const COMPOSER_PACKAGE_REGEX = '/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$/'; + /** * The lock info for the stage. * @@ -327,12 +351,12 @@ class Stage implements LoggerAwareInterface { // Change the runtime and dev requirements as needed, but don't update // the installed packages yet. if ($runtime) { - $this->validatePackageNames($runtime); + self::validateRequirements($runtime); $command = array_merge(['require', '--no-update'], $runtime); $this->stager->stage($command, $active_dir, $stage_dir, NULL, $timeout); } if ($dev) { - $this->validatePackageNames($dev); + self::validateRequirements($dev); $command = array_merge(['require', '--dev', '--no-update'], $dev); $this->stager->stage($command, $active_dir, $stage_dir, NULL, $timeout); } @@ -726,10 +750,10 @@ class Stage implements LoggerAwareInterface { * Validates a set of package names. * * Package names are considered invalid if they look like Drupal project - * names. The only exceptions to this are `php` and `composer`, which Composer - * treats as legitimate requirements. + * names. The only exceptions to this are platform requirements, like `php`, + * `composer`, or `ext-json`, which are legitimate to Composer. * - * @param string[] $package_versions + * @param string[] $requirements * A set of package names (with or without version constraints), as passed * to ::require(). * @@ -738,10 +762,18 @@ class Stage implements LoggerAwareInterface { * * @see https://getcomposer.org/doc/articles/composer-platform-dependencies.md */ - protected function validatePackageNames(array $package_versions): void { - foreach ($package_versions as $package_name) { - if (!ComposerUtility::isValidRequirement($package_name)) { - throw new \InvalidArgumentException("Invalid package name '$package_name'."); + protected static function validateRequirements(array $requirements): void { + $version_parser = new VersionParser(); + + foreach ($requirements as $requirement) { + $parts = explode(':', $requirement, 2); + $name = $parts[0]; + + if (!preg_match(self::COMPOSER_PLATFORM_PACKAGE_REGEX, $name) && !preg_match(self::COMPOSER_PACKAGE_REGEX, $name)) { + throw new \InvalidArgumentException("Invalid package name '$name'."); + } + if (count($parts) > 1) { + $version_parser->parseConstraints($parts[1]); } } } diff --git a/package_manager/tests/src/Kernel/StageTest.php b/package_manager/tests/src/Kernel/StageTest.php index 1c5cf90f3b2b4c96fdd10db0aa3bccdd713f1ee0..6c96779c1f4bbd1443139ecac5715cd171e498a0 100644 --- a/package_manager/tests/src/Kernel/StageTest.php +++ b/package_manager/tests/src/Kernel/StageTest.php @@ -544,42 +544,6 @@ class StageTest extends PackageManagerKernelTestBase { $this->assertFalse($logger->hasRecord($warning_message, LogLevel::WARNING)); } - /** - * @covers ::validatePackageNames - * - * @param string $package_name - * The package name. - * @param bool $is_invalid - * TRUE if the given package name is invalid and will cause an exception, - * FALSE otherwise. - * - * @dataProvider providerValidatePackageNames - */ - public function testValidatePackageNames(string $package_name, bool $is_invalid): void { - $stage = $this->createStage(); - $stage->create(); - if ($is_invalid) { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage("Invalid package name '$package_name'."); - } - $stage->require([$package_name]); - // If we got here, the package name is valid and we just need to assert something so PHPUnit doesn't complain. - $this->assertTrue(TRUE); - } - - /** - * Data provider for testValidatePackageNames. - * - * @return array[] - * The test cases. - */ - public function providerValidatePackageNames(): array { - return [ - 'Full package name' => ['drupal/semver_test', FALSE], - 'Bare Drupal project name' => ['semver_test', TRUE], - ]; - } - /** * Tests that ignored paths are collected before create and apply. */ diff --git a/package_manager/tests/src/Unit/StageTest.php b/package_manager/tests/src/Unit/StageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bc5bf620e97c417753532a0a3a68d139e3ea3bf7 --- /dev/null +++ b/package_manager/tests/src/Unit/StageTest.php @@ -0,0 +1,123 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\package_manager\Unit; + +use Drupal\package_manager\Stage; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\package_manager\Stage + * @group package_manager + * @internal + */ +class StageTest extends UnitTestCase { + + /** + * @covers ::validateRequirements + * + * @param string|null $expected_exception + * The exception class that should be thrown, or NULL if there should not be + * any exception. + * @param string $requirement + * The requirement (package name and optional constraint) to validate. + * + * @dataProvider providerValidateRequirements + */ + public function testValidateRequirements(?string $expected_exception, string $requirement): void { + $reflector = new \ReflectionClass(Stage::class); + $method = $reflector->getMethod('validateRequirements'); + $method->setAccessible(TRUE); + + if ($expected_exception) { + $this->expectException($expected_exception); + } + else { + $this->assertNull($expected_exception); + } + + $method->invoke(NULL, [$requirement]); + } + + /** + * Data provider for testValidateRequirements. + * + * @return array[] + * The test cases. + */ + public function providerValidateRequirements(): array { + return [ + // Valid requirements. + [NULL, 'vendor/package'], + [NULL, 'vendor/snake_case'], + [NULL, 'vendor/kebab-case'], + [NULL, 'vendor/with.dots'], + [NULL, '1vendor2/3package4'], + [NULL, 'vendor/package:1'], + [NULL, 'vendor/package:1.2'], + [NULL, 'vendor/package:1.2.3'], + [NULL, 'vendor/package:1.x'], + [NULL, 'vendor/package:^1'], + [NULL, 'vendor/package:~1'], + [NULL, 'vendor/package:>1'], + [NULL, 'vendor/package:<1'], + [NULL, 'vendor/package:>=1'], + [NULL, 'vendor/package:>1 <2'], + [NULL, 'vendor/package:1 || 2'], + [NULL, 'vendor/package:>=1,<1.1.0'], + [NULL, 'vendor/package:1a'], + [NULL, 'vendor/package:*'], + [NULL, 'vendor/package:dev-master'], + [NULL, 'vendor/package:*@dev'], + [NULL, 'vendor/package:@dev'], + [NULL, 'vendor/package:master@dev'], + [NULL, 'vendor/package:master@beta'], + [NULL, 'php'], + [NULL, 'php:8'], + [NULL, 'php:8.0'], + [NULL, 'php:^8.1'], + [NULL, 'php:~8.1'], + [NULL, 'php-64bit'], + [NULL, 'composer'], + [NULL, 'composer-plugin-api'], + [NULL, 'composer-plugin-api:1'], + [NULL, 'ext-json'], + [NULL, 'ext-json:1'], + [NULL, 'ext-pdo_mysql'], + [NULL, 'ext-pdo_mysql:1'], + [NULL, 'lib-curl'], + [NULL, 'lib-curl:1'], + [NULL, 'lib-curl-zlib'], + [NULL, 'lib-curl-zlib:1'], + + // Invalid requirements. + [\InvalidArgumentException::class, ''], + [\InvalidArgumentException::class, ' '], + [\InvalidArgumentException::class, '/'], + [\InvalidArgumentException::class, 'php8'], + [\InvalidArgumentException::class, 'package'], + [\InvalidArgumentException::class, 'vendor\package'], + [\InvalidArgumentException::class, 'vendor//package'], + [\InvalidArgumentException::class, 'vendor/package1 vendor/package2'], + [\InvalidArgumentException::class, 'vendor/package/extra'], + [\UnexpectedValueException::class, 'vendor/package:a'], + [\UnexpectedValueException::class, 'vendor/package:'], + [\UnexpectedValueException::class, 'vendor/package::'], + [\UnexpectedValueException::class, 'vendor/package::1'], + [\UnexpectedValueException::class, 'vendor/package:1:2'], + [\UnexpectedValueException::class, 'vendor/package:develop@dev@dev'], + [\UnexpectedValueException::class, 'vendor/package:develop@'], + [\InvalidArgumentException::class, 'vEnDor/pAcKaGe'], + [\InvalidArgumentException::class, '_vendor/package'], + [\InvalidArgumentException::class, '_vendor/_package'], + [\InvalidArgumentException::class, 'vendor_/package'], + [\InvalidArgumentException::class, '_vendor/package_'], + [\InvalidArgumentException::class, 'vendor/package-'], + [\InvalidArgumentException::class, 'php-'], + [\InvalidArgumentException::class, 'ext'], + [\InvalidArgumentException::class, 'lib'], + ]; + } + +}