Skip to content

Commit 5187a0c

Browse files
authored
BC: Rework licenses validation (#249)
- complete SPDX license expression validation/detection - license models do no validations - license factory rework Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 8dfbea8 commit 5187a0c

30 files changed

+703
-471
lines changed

.php-cs-fixer.dist.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
'phpdoc_order' => true,
4848
'phpdoc_to_comment' => [
4949
'ignored_tags' => [
50-
// 'psalm-var', // needed when PSALM introduced some issues that only manual hints can solve
50+
// needed when PSALM introduced some issues that only manual hints can solve
51+
// 'psalm-var',
52+
// 'psalm-suppress',
5153
],
5254
],
5355
]

HISTORY.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
1111
* Removed support for PHP v7.4 ([#114] via [#125])
1212
* Removed support for PHP v8.0 (via [#204])
1313
* Changed models' aggregation properties to be no longer optional ([#66] via [#131])
14+
* CHanged models to be less restrictive ([#247] via [#249])
1415
* Streamlined repository data structures to follow a common method naming scheme (via [#131])
1516
* Enumeration-like classes were converted to native [PHP Enumerations](https://www.php.net/manual/en/language.types.enumerations.php) ([#140] via [#204])
1617
* Added
@@ -50,9 +51,16 @@ All notable changes to this project will be documented in this file.
5051
* BREAKING: method `isValidValue()` was removed (via [#204])
5152
* `CycloneDX\Core\Factories` namespace
5253
* `LicenseFactory` class
54+
* BREAKING: check whether something is a valid SPDX Expression is now complete, was best effort implementation ([#247] via [#249])
55+
This affects all methods that potentially would create `LicenseExpression` models.
56+
Utilizes [`composer/spdx-licenses`](https://packagist.org/packages/composer/spdx-licenses).
57+
* BREAKING: changed constructor method `__construct()` (via [#249])
5358
* BREAKING: removed method `makeDisjunctiveFromExpression()` ([#163] vial [#166])
59+
* BREAKING: removed method `setSpdxLicenseValidator()` (via [#249])
60+
* BREAKING: renamed method `getSpdxLicenseValidator()` -> `getLicenseIdentifiers()` (via [#249])
5461
* BREAKING: renamed method `makeDisjunctiveWithId()` -> `makeSpdxLicense()` ([#164] vial [#168])
5562
* BREAKING: renamed method `makeDisjunctiveWithName()` -> `makeNamedLicense()` ([#164] vial [#168])
63+
* Added new method `getSpdxLicenses()` (via [#249])
5664
* `\CycloneDX\Core\Models` namespace
5765
* `Bom` class
5866
* BREAKING: changed constructor to no longer accept components ([#187] via [#188])
@@ -61,8 +69,8 @@ All notable changes to this project will be documented in this file.
6169
Also changed parameter & return type to non-nullable, was nullable ([#66] via [#131])
6270
* BREAKING: renamed methods `{get,set}MetaData()` -> `{get,set}Metadata()` ([#133] via [#131])
6371
Also changed parameter & return type to non-nullable, was nullable ([#66] via [#131])
64-
* Added `{get,set}Properties()` ([#228] via [#229])
65-
* Added `{get,set}SerialNumber()` (via [#186])
72+
* Added new methods `{get,set}Properties()` ([#228] via [#229])
73+
* Added new methods `{get,set}SerialNumber()` (via [#186])
6674
* `Component` class
6775
* BREAKING: renamed methods `{get,set}DependenciesBomRefRepository()` -> `{get,set}Dependencies()` ([#133] via [#131])
6876
Also changed parameter & return type to non-nullable, was nullable ([#66] via [#131])
@@ -76,10 +84,10 @@ All notable changes to this project will be documented in this file.
7684
This affects constructor arguments, and affects methods `{get,set}Version()`.
7785
* BREAKING: changed property `type` to be of type `\CycloneDX\Core\Enum\ComponentType` ([#140] via [#204])
7886
This affects constructor arguments, and affects methods `{get,set}Type()`.
79-
* Added `{get,set}Author()` ([#184] via [#185])
80-
* Added `{get,set}Copyright()` ([#238] via [#239])
81-
* Added `{get,set}Evidence()` ([#238] via [#241])
82-
* Added `{get,set}Properties()` ([#228] via [#165])
87+
* Added new methods `{get,set}Author()` ([#184] via [#185])
88+
* Added new methods `{get,set}Copyright()` ([#238] via [#239])
89+
* Added new methods `{get,set}Evidence()` ([#238] via [#241])
90+
* Added new methods `{get,set}Properties()` ([#228] via [#165])
8391
* Added new class `ComponentEvidence` ([#238] via [#241])
8492
* `ExternalReference` class
8593
* BREAKING: renamed methods `{get,set}HashRepository()` -> `{get,set}Hashes()` ([#133] via [#131])
@@ -93,6 +101,14 @@ All notable changes to this project will be documented in this file.
93101
* BREAKING: renamed class to `NamedLicense` ([#164] via [#168])
94102
* `DisjunctiveLicenseWithId` class
95103
* BREAKING: renamed class to `SpdxLicense` ([#164] via [#168])
104+
* BREAKING: removed factory method `makeValidated()` ([#247] via [#249])
105+
To assert valid values use `\CycloneDX\Core\Factories\LicenseFactory::makeSpdxLicense()`.
106+
* Changed: constructor `__construct()` is public now, was private ([#247] via [#249])
107+
* Added new method `setId()` ([#247] via [#249])
108+
* `LicenseExpression` class
109+
* BREAKING: constructor `__construct()` and method `setExpression()` no longer do validation, but only assert that the parameter is no empty string. ([#247] ia [#249])
110+
To assert valid values use `\CycloneDX\Core\Factories\LicenseFactory::makeExpression()`.
111+
* BREAKING: removed method `isValid()` ([#247] via [#249])
96112
* `MetaData` class
97113
* BREAKING: renamed class to `Metadata` ([#133] via [#131])
98114
Even though PHP is case-insensitive with class names, autoloaders may be case-sensitive. Therefore, this is considered a breaking change.
@@ -135,10 +151,10 @@ All notable changes to this project will be documented in this file.
135151
* BREAKING: removed method `makeForDisjunctiveLicenseRepository()` (via [#131])
136152
* BREAKING: removed method `makeForHashRepositonary()` - use `makeForHashDictionary()` instead ([#133] via [#131])
137153
* BREAKING: removed method `setSpec()` (via [#131])
138-
* Added method `makeForComponentEvidence()` ([#238] via [#241])
139-
* Added method `makeForHashDictionary()` ([#133] via [#131])
140-
* Added method `makeForLicense()` (via [#131])
141-
* Added method `makeForLicenseRepository()` (via [#131])
154+
* Added new method `makeForComponentEvidence()` ([#238] via [#241])
155+
* Added new method `makeForHashDictionary()` ([#133] via [#131])
156+
* Added new method `makeForLicense()` (via [#131])
157+
* Added new method `makeForLicenseRepository()` (via [#131])
142158
* `{DOM,JSON}\Normalizers` namespaces
143159
* BREAKING: removed classes `DisjunctiveLicenseNormalizer` - use `LicenseNormalizer` instead (via [#131])
144160
* BREAKING: removed classes `LicenseExpressionNormalizer` - use `LicenseNormalizer` instead (via [#131])
@@ -159,7 +175,10 @@ All notable changes to this project will be documented in this file.
159175
* `JSON\Normalizers\ExternalReferenceNormalizer` class
160176
* BREAKING: method `normalize()` may throw `\UnexpectedValueException` when the url is invalid to format "ini-reference" (via [#151])
161177
* `\CycloneDX\Core\Spdx` namespace
162-
* BREAKING: renamed the class `License` -> `LicenseValidator` ([#133] via [#143])
178+
* BREAKING: renamed the class `License` -> `LicenseIdentifiers` ([#133] via [#143], [#249])
179+
* BREAKING: renamed method `getLicense()` -> `fixLicense()` (via [#249])
180+
* BREAKING: renamed method `getLicenses()` -> `getKnownLicenses()`, and removed keys from return value (via [#249])
181+
* BREAKING: renamed method `validate()` -> `isKnownLicense()` (via [#249])
163182
* `\CycloneDX\Core\Spec` namespace
164183
* BREAKING: completely reworked everything ([#139] via [#142], [#174], [#204])
165184
See the code base for references
@@ -220,6 +239,8 @@ All notable changes to this project will be documented in this file.
220239
[#238]: https://github.com/CycloneDX/cyclonedx-php-library/issues/238
221240
[#239]: https://github.com/CycloneDX/cyclonedx-php-library/pull/239
222241
[#241]: https://github.com/CycloneDX/cyclonedx-php-library/pull/241
242+
[#247]: https://github.com/CycloneDX/cyclonedx-php-library/issues/247
243+
[#249]: https://github.com/CycloneDX/cyclonedx-php-library/pull/249
223244

224245
## 1.6.3 - 2022-09-15
225246

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"ext-dom": "*",
3030
"ext-json": "*",
3131
"ext-libxml": "*",
32+
"composer/spdx-licenses": "^1.5",
3233
"opis/json-schema": "^2.0",
3334
"package-url/packageurl-php": "^1.0"
3435
},

phpunit.dist.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
beStrictAboutCoverageMetadata="true"
99
beStrictAboutOutputDuringTests="true"
1010
beStrictAboutTodoAnnotatedTests="true"
11+
displayDetailsOnIncompleteTests="true"
12+
displayDetailsOnSkippedTests="true"
13+
displayDetailsOnTestsThatTriggerNotices="false"
1114
displayDetailsOnTestsThatTriggerDeprecations="true"
15+
displayDetailsOnTestsThatTriggerWarnings="true"
16+
displayDetailsOnTestsThatTriggerErrors="true"
1217
failOnRisky="true"
1318
failOnWarning="true"
1419
defaultTestSuite="default"

src/Core/Factories/LicenseFactory.php

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,86 +23,83 @@
2323

2424
namespace CycloneDX\Core\Factories;
2525

26+
use Composer\Spdx\SpdxLicenses;
2627
use CycloneDX\Core\Models\License\LicenseExpression;
2728
use CycloneDX\Core\Models\License\NamedLicense;
2829
use CycloneDX\Core\Models\License\SpdxLicense;
29-
use CycloneDX\Core\Spdx\LicenseValidator as SpdxLicenseValidator;
30+
use CycloneDX\Core\Spdx\LicenseIdentifiers;
3031
use DomainException;
31-
use UnexpectedValueException;
3232

3333
class LicenseFactory
3434
{
35-
private ?SpdxLicenseValidator $spdxLicenseValidator;
36-
37-
public function __construct(?SpdxLicenseValidator $spdxLicenseValidator = null)
38-
{
39-
$this->spdxLicenseValidator = $spdxLicenseValidator;
35+
public function __construct(
36+
private readonly LicenseIdentifiers $licenseIdentifiers = new LicenseIdentifiers(),
37+
private readonly SpdxLicenses $spdxLicenses = new SpdxLicenses()
38+
) {
4039
}
4140

42-
/**
43-
* @psalm-assert SpdxLicenseValidator $this->spdxLicenseValidator
44-
*
45-
* @throws UnexpectedValueException when SpdxLicenseValidator is missing
46-
*/
47-
public function getSpdxLicenseValidator(): SpdxLicenseValidator
41+
public function getLicenseIdentifiers(): LicenseIdentifiers
4842
{
49-
return $this->spdxLicenseValidator
50-
?? throw new UnexpectedValueException('Missing spdxLicenseValidator');
43+
return $this->licenseIdentifiers;
5144
}
5245

53-
/**
54-
* @psalm-assert SpdxLicenseValidator $this->spdxLicenseValidator
55-
*/
56-
public function setSpdxLicenseValidator(SpdxLicenseValidator $spdxLicenseValidator): static
46+
public function getSpdxLicenses(): SpdxLicenses
5747
{
58-
$this->spdxLicenseValidator = $spdxLicenseValidator;
59-
60-
return $this;
48+
return $this->spdxLicenses;
6149
}
6250

63-
/**
64-
* @see makeExpression()
65-
* @see makeDisjunctive()
66-
*/
67-
public function makeFromString(string $license): NamedLicense|SpdxLicense|LicenseExpression
51+
public function makeFromString(string $license): SpdxLicense|LicenseExpression|NamedLicense
6852
{
53+
try {
54+
return $this->makeSpdxLicense($license);
55+
} catch (\DomainException) {
56+
/* pass */
57+
}
6958
try {
7059
return $this->makeExpression($license);
71-
} catch (DomainException) {
72-
return $this->makeDisjunctive($license);
60+
} catch (\DomainException) {
61+
/* pass */
7362
}
63+
64+
return $this->makeNamedLicense($license);
7465
}
7566

76-
/**
77-
* @throws DomainException if the expression was invalid
78-
*/
79-
public function makeExpression(string $license): LicenseExpression
67+
public function makeDisjunctive(string $license): SpdxLicense|NamedLicense
8068
{
81-
return new LicenseExpression($license);
69+
try {
70+
return $this->makeSpdxLicense($license);
71+
} catch (\DomainException) {
72+
return $this->makeNamedLicense($license);
73+
}
8274
}
8375

8476
/**
85-
* @see makeSpdxLicense()
86-
* @see makeNamedLicense()
77+
* @throws DomainException when the SPDX license expressions was invalid
8778
*/
88-
public function makeDisjunctive(string $license): SpdxLicense|NamedLicense
79+
public function makeExpression(string $license): LicenseExpression
8980
{
9081
try {
91-
return $this->makeSpdxLicense($license);
92-
} catch (UnexpectedValueException|DomainException) {
93-
return $this->makeNamedLicense($license);
82+
$valid = $this->spdxLicenses->validate($license);
83+
} catch (\InvalidArgumentException) {
84+
$valid = false;
9485
}
86+
if ($valid) {
87+
return new LicenseExpression($license);
88+
}
89+
throw new DomainException("invalid SPDX license expressions: $license");
9590
}
9691

9792
/**
98-
* @throws DomainException when the SPDX license is invalid
99-
* @throws UnexpectedValueException when SpdxLicenseValidator is missing
100-
*
101-
* @SuppressWarnings(PHPMD.StaticAccess)
93+
* @throws DomainException when the SPDX license ID is unknown
10294
*/
10395
public function makeSpdxLicense(string $license): SpdxLicense
10496
{
105-
return SpdxLicense::makeValidated($license, $this->getSpdxLicenseValidator());
97+
$fixed = $this->licenseIdentifiers->fixLicense($license);
98+
if (null === $fixed) {
99+
throw new DomainException("unknown SPDX license ID: $license");
100+
}
101+
102+
return new SpdxLicense($fixed);
106103
}
107104

108105
public function makeNamedLicense(string $license): NamedLicense

src/Core/Models/License/LicenseExpression.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,18 @@
2626
use DomainException;
2727

2828
/**
29+
* (SPDX) License Expression.
30+
*
31+
* No validation is done internally.
32+
* You may validate with {@see \Composer\Spdx\SpdxLicenses::isValidLicenseString()}.
33+
*
2934
* @author jkowalleck
3035
*/
3136
class LicenseExpression
3237
{
3338
/**
39+
* @psalm-var non-empty-string
40+
*
3441
* @psalm-suppress PropertyNotSetInConstructor
3542
*/
3643
private string $expression;
@@ -41,32 +48,29 @@ public function getExpression(): string
4148
}
4249

4350
/**
44-
* @throws DomainException if the expression was invalid
51+
* @psalm-assert non-empty-string $expression
52+
*
53+
* @throws DomainException if `$expression` is empty string
4554
*
4655
* @return $this
4756
*/
4857
public function setExpression(string $expression): static
4958
{
50-
$this->expression = self::isValid($expression)
51-
? $expression
52-
: throw new DomainException("Invalid expression: $expression");
59+
if ('' === $expression) {
60+
throw new DomainException('expression must not be empty');
61+
}
62+
$this->expression = $expression;
5363

5464
return $this;
5565
}
5666

5767
/**
58-
* @throws DomainException if the expression was invalid
68+
* @psalm-assert non-empty-string $expression
69+
*
70+
* @throws DomainException if `$expression` is empty string
5971
*/
6072
public function __construct(string $expression)
6173
{
6274
$this->setExpression($expression);
6375
}
64-
65-
public static function isValid(string $expression): bool
66-
{
67-
// smallest known: (A or B)
68-
return \strlen($expression) >= 8
69-
&& '(' === $expression[0]
70-
&& ')' === $expression[-1];
71-
}
7276
}

src/Core/Models/License/NamedLicense.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@ class NamedLicense
3434

3535
/**
3636
* If SPDX does not define the license used, this field may be used to provide the license name.
37+
*
38+
* Implementation detail: allow empty strings.
39+
*
40+
* @psalm-suppress PropertyNotSetInConstructor
3741
*/
38-
private ?string $name = null;
42+
private string $name;
3943

40-
public function getName(): ?string
44+
public function getName(): string
4145
{
4246
return $this->name;
4347
}

0 commit comments

Comments
 (0)