diff --git a/HISTORY.md b/HISTORY.md index 851a1a72..2185c45e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. ## unreleased +* Added + * CLI got a new switch `--no-version-normalization`. (via [#138]) + That allows to omit component version-string normalization. + Per default this plugin will normalize version strings by stripping leading "v". + This is a compatibility-switch. The next major-version of this plugin will not modify component versions. (see [#102]) + +[#138]: https://github.com/CycloneDX/cyclonedx-php-composer/pull/138 +[#102]: https://github.com/CycloneDX/cyclonedx-php-composer/issues/102 + + + ## 3.6.0 - 2021-10-15 * Added diff --git a/README.md b/README.md index c4bb35a1..b48040f8 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,12 @@ Options: --exclude-plugins Exclude composer plugins --spec-version=SPEC-VERSION Which version of CycloneDX spec to use. Values: "1.1", "1.2", "1.3" [default: "1.3"] - --no-validate Dont validate the resulting output + --no-validate Don't validate the resulting output --mc-version=MC-VERSION Version of the main component. This will override auto-detection. + --no-version-normalization Don't normalize component version strings. + Per default this plugin will normalize version strings by stripping leading "v". + This is a compatibility-switch. The next major-version of this plugin will not modify component versions. -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version diff --git a/src/Builders/ComponentBuilder.php b/src/Builders/ComponentBuilder.php index 0519f583..0d48d7d5 100644 --- a/src/Builders/ComponentBuilder.php +++ b/src/Builders/ComponentBuilder.php @@ -48,12 +48,17 @@ class ComponentBuilder /** @var PackageUrlFactory */ private $packageUrlFactory; + /** @var bool */ + private $enableVersionNormalization; + public function __construct( LicenseFactory $licenseFactory, - PackageUrlFactory $packageUrlFactory + PackageUrlFactory $packageUrlFactory, + bool $enableVersionNormalization = true ) { $this->licenseFactory = $licenseFactory; $this->packageUrlFactory = $packageUrlFactory; + $this->enableVersionNormalization = $enableVersionNormalization; } public function getLicenseFactory(): LicenseFactory @@ -66,6 +71,13 @@ public function getPackageUrlFactory(): PackageUrlFactory return $this->packageUrlFactory; } + public function setVersionNormalization(bool $enableVersionNormalization): self + { + $this->enableVersionNormalization = $enableVersionNormalization; + + return $this; + } + /** * @throws UnexpectedValueException if the given package does not provide a name or version */ @@ -144,17 +156,22 @@ private function getPackageVersion(PackageInterface $package): string return $version; } - // Versions of Composer packages may be prefixed with "v". - // * This prefix appears to be problematic for CPE and PURL matching and thus is removed here. - // * - // * See for example {@link https://ossindex.sonatype.org/component/pkg:composer/phpmailer/phpmailer@v6.0.7} - // * vs {@link https://ossindex.sonatype.org/component/pkg:composer/phpmailer/phpmailer@6.0.7}. - // - // A _numeric_ version can be prefixed with 'v'. - // Strip leading 'v' must not be applied if the "version" is actually a branch name, - // which is totally fine in the composer ecosystem. - if (1 === preg_match('/^v\\d/', $version)) { - return substr($version, 1); + if ($this->enableVersionNormalization) { + // Versions of Composer packages may be prefixed with "v". + // * This prefix appears to be problematic for CPE and PURL matching and thus is removed here. + // * + // * See for example {@link https://ossindex.sonatype.org/component/pkg:composer/phpmailer/phpmailer@v6.0.7} + // * vs {@link https://ossindex.sonatype.org/component/pkg:composer/phpmailer/phpmailer@6.0.7}. + // + // A _numeric_ version can be prefixed with 'v'. + // Strip leading 'v' must not be applied if the "version" is actually a branch name, + // which is totally fine in the composer ecosystem. + // + // will be removed via https://github.com/CycloneDX/cyclonedx-php-composer/issues/102 + // @TODO remove the whole normalizer with next major version + if (1 === preg_match('/^v\\d/', $version)) { + return substr($version, 1); + } } return $version; diff --git a/src/MakeBom/Command.php b/src/MakeBom/Command.php index b34c5218..ff391d2d 100644 --- a/src/MakeBom/Command.php +++ b/src/MakeBom/Command.php @@ -58,7 +58,7 @@ class Command extends BaseCommand /** * @var \CycloneDX\Composer\Builders\BomBuilder */ - private $bomFactory; + private $bomBuilder; /** * @var ToolUpdater|null @@ -77,7 +77,7 @@ public function __construct( ) { $this->options = $options; $this->factory = $factory; - $this->bomFactory = $bomFactory; + $this->bomBuilder = $bomFactory; $this->toolUpdater = $toolUpdater; parent::__construct($name); } @@ -117,6 +117,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $io->writeErrorRaw(__METHOD__.' Options: '.print_r($this->options, true), true, IOInterface::DEBUG); + $this->bomBuilder->getComponentBuilder()->setVersionNormalization( + false === $this->options->omitVersionNormalization + ); + $this->updateTool(); $bomString = $this->makeBomString( @@ -164,7 +168,7 @@ private function makeBom(Composer $composer): Bom $components = $this->factory->makeLockerFromComposerForOptions($composer, $this->options); $rootComponentVersionOverride = $this->options->mainComponentVersion; - $bom = $this->bomFactory->makeForPackageWithRequires($rootPackage, $components, $rootComponentVersionOverride); + $bom = $this->bomBuilder->makeForPackageWithRequires($rootPackage, $components, $rootComponentVersionOverride); $io->writeErrorRaw('Bom: '.print_r($bom, true), true, IOInterface::DEBUG); @@ -264,7 +268,7 @@ private function updateTool(): ?bool $lockerRepo = $locker->getLockedRepository($withDevReqs); // @TODO better use the installed-repo than the lockerRepo - as of milestone v4 - return $updater->updateTool($this->bomFactory->getTool(), $lockerRepo); + return $updater->updateTool($this->bomBuilder->getTool(), $lockerRepo); } catch (\Exception $exception) { return false; } diff --git a/src/MakeBom/Options.php b/src/MakeBom/Options.php index 5e4564b6..2f0dae71 100644 --- a/src/MakeBom/Options.php +++ b/src/MakeBom/Options.php @@ -46,6 +46,10 @@ class Options private const SWITCH_EXCLUDE_PLUGINS = 'exclude-plugins'; private const SWITCH_NO_VALIDATE = 'no-validate'; + // added in preparation for https://github.com/CycloneDX/cyclonedx-php-composer/issues/102 + // @TODO remove with next major version + private const SWITCH_NO_VERSION_NORMALIZATION = 'no-version-normalization'; + private const ARGUMENT_COMPOSER_FILE = 'composer-file'; public const OUTPUT_FORMAT_XML = 'XML'; @@ -111,7 +115,7 @@ public function configureCommand(Command $command): void self::SWITCH_NO_VALIDATE, null, InputOption::VALUE_NONE, - 'Dont validate the resulting output' + 'Don\'t validate the resulting output' ) ->addOption( self::OPTION_MAIN_COMPONENT_VERSION, @@ -121,6 +125,14 @@ public function configureCommand(Command $command): void 'This will override auto-detection.', null ) + ->addOption( + self::SWITCH_NO_VERSION_NORMALIZATION, + null, + InputOption::VALUE_NONE, + 'Don\'t normalize component version strings.'.\PHP_EOL. + 'Per default this plugin will normalize version strings by stripping leading "v".'.\PHP_EOL. + 'This is a compatibility-switch. The next major-version of this plugin will not modify component versions.' + ) ->addArgument( self::ARGUMENT_COMPOSER_FILE, InputArgument::OPTIONAL, @@ -151,6 +163,13 @@ public function configureCommand(Command $command): void */ public $excludePlugins = false; + /** + * @var bool + * @readonly + * @psalm-allow-private-mutation + */ + public $omitVersionNormalization = false; + /** * @var string * @psalm-var Options::OUTPUT_FORMAT_* @@ -214,6 +233,7 @@ public function setFromInput(InputInterface $input): self $excludeDev = false !== $input->getOption(self::SWITCH_EXCLUDE_DEV); $excludePlugins = false !== $input->getOption(self::SWITCH_EXCLUDE_PLUGINS); $skipOutputValidation = false !== $input->getOption(self::SWITCH_NO_VALIDATE); + $omitVersionNormalization = false !== $input->getOption(self::SWITCH_NO_VERSION_NORMALIZATION); $outputFile = $input->getOption(self::OPTION_OUTPUT_FILE); \assert(null === $outputFile || \is_string($outputFile)); $composerFile = $input->getArgument(self::ARGUMENT_COMPOSER_FILE); @@ -233,6 +253,7 @@ public function setFromInput(InputInterface $input): self $this->excludePlugins = $excludePlugins; $this->outputFormat = $outputFormat; $this->skipOutputValidation = $skipOutputValidation; + $this->omitVersionNormalization = $omitVersionNormalization; $this->outputFile = \is_string($outputFile) && '' !== $outputFile ? $outputFile : self::OUTPUT_FILE_DEFAULT[$outputFormat]; diff --git a/tests/Builders/ComponentBuilderTest.php b/tests/Builders/ComponentBuilderTest.php index 14a3b193..9756c3ac 100644 --- a/tests/Builders/ComponentBuilderTest.php +++ b/tests/Builders/ComponentBuilderTest.php @@ -130,12 +130,14 @@ public function testMakeFromPackageEmptPurlOnThrow(): void public function testMakeFromPackage( PackageInterface $package, Component $expected, + bool $enableVersionNormalization = true, ?LicenseFactory $licenseFactory = null ): void { $packageUrlFactory = $this->createMock(PackageUrlFactory::class); $builder = new ComponentBuilder( $licenseFactory ?? $this->createStub(LicenseFactory::class), - $packageUrlFactory + $packageUrlFactory, + $enableVersionNormalization ); $purlMadeFromComponent = null; @@ -172,6 +174,7 @@ public function dpMakeFromPackage(): \Generator (new Component('library', 'some-library', '1.2.3')) ->setPackageUrl((new PackageUrl('composer', 'some-library'))->setVersion('1.2.3')) ->setBomRefValue('pkg:composer/some-library@1.2.3'), + true, null, ]; @@ -187,6 +190,7 @@ public function dpMakeFromPackage(): \Generator (new Component('application', 'some-project', '1.2.3')) ->setPackageUrl((new PackageUrl('composer', 'some-project'))->setVersion('1.2.3')) ->setBomRefValue('pkg:composer/some-project@1.2.3'), + true, null, ]; @@ -202,6 +206,7 @@ public function dpMakeFromPackage(): \Generator (new Component('application', 'some-composer-plugin', '1.2.3')) ->setPackageUrl((new PackageUrl('composer', 'some-composer-plugin'))->setVersion('1.2.3')) ->setBomRefValue('pkg:composer/some-composer-plugin@1.2.3'), + true, null, ]; @@ -218,6 +223,7 @@ public function dpMakeFromPackage(): \Generator (new Component('library', 'some-inDev', 'dev-master')) ->setPackageUrl((new PackageUrl('composer', 'some-inDev'))->setVersion('dev-master')) ->setBomRefValue('pkg:composer/some-inDev@dev-master'), + true, null, ]; @@ -233,6 +239,7 @@ public function dpMakeFromPackage(): \Generator (new Component('library', 'some-noVersion', RootPackage::DEFAULT_PRETTY_VERSION)) ->setPackageUrl((new PackageUrl('composer', 'some-noVersion'))->setVersion(null)) ->setBomRefValue('pkg:composer/some-noVersion'), + true, null, ]; @@ -266,7 +273,28 @@ public function dpMakeFromPackage(): \Generator ->setLicense($license) ->setHashRepository(new HashRepository([HashAlgorithm::SHA_1 => '12345678901234567890123456789012'])) ->setBomRefValue('pkg:composer/my/package@1.2.3?checksum=sha1:12345678901234567890123456789012'), + true, $licenseFactory, ]; + + yield 'library with non-normalized version' => [ + $this->createConfiguredMock( + CompletePackageInterface::class, + [ + 'getPrettyName' => 'my/package', + 'getPrettyVersion' => 'v1.2.3', + ] + ), + (new Component('library', 'package', 'v1.2.3')) + ->setGroup('my') + ->setPackageUrl( + (new PackageUrl('composer', 'package')) + ->setNamespace('my') + ->setVersion('v1.2.3') + ) + ->setBomRefValue('pkg:composer/my/package@v1.2.3'), + false, + null, + ]; } } diff --git a/tests/MakeBom/OptionsTest.php b/tests/MakeBom/OptionsTest.php index 61e97665..e68d5a39 100644 --- a/tests/MakeBom/OptionsTest.php +++ b/tests/MakeBom/OptionsTest.php @@ -90,6 +90,9 @@ public static function dpSetFromInput(): \Generator ['--spec-version=1.1', 'specVersion', Version::V_1_1], ['--spec-version=1.2', 'specVersion', Version::V_1_2], ['--spec-version=1.3', 'specVersion', Version::V_1_3], + /* @see \CycloneDX\Composer\MakeBom\Options::$omitVersionNormalization */ + 'omitVersionNormalization default' => ['', 'omitVersionNormalization', false], + ['--no-version-normalization', 'omitVersionNormalization', true], /* @see \CycloneDX\Composer\MakeBom\Options::$composerFile */ 'composerFile default to null' => ['', 'composerFile', null], ['my/project/composer.json', 'composerFile', 'my/project/composer.json'],