Skip to content

Commit b2595cf

Browse files
authored
fix: schema-invalid CycloneDX when running PEP639 analysis (#828)
fixes #826 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 74f07e1 commit b2595cf

File tree

54 files changed

+2873
-15
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2873
-15
lines changed

cyclonedx_py/_internal/environment.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
2828

2929
from cyclonedx.model import Property
30-
from cyclonedx.model.component import Component, ComponentType
30+
from cyclonedx.model.component import Component, ComponentEvidence, ComponentType
3131
from packageurl import PackageURL
3232
from packaging.requirements import Requirement
3333

3434
from . import BomBuilder, PropertyName, PurlTypePypi
3535
from .cli_common import add_argument_mc_type, add_argument_pyproject
36-
from .utils.cdx import licenses_fixup, make_bom
36+
from .utils.cdx import find_LicenseExpression, licenses_fixup, make_bom
3737
from .utils.packaging import metadata2extrefs, metadata2licenses, normalize_packagename
3838
from .utils.pep610 import PackageSourceArchive, PackageSourceVcs, packagesource2extref, packagesource4dist
3939
from .utils.pep639 import dist2licenses as dist2licenses_pep639
@@ -183,10 +183,21 @@ def __add_components(self, bom: 'Bom',
183183
# path of dist-package on disc? naaa... a package may have multiple files/folders on disc
184184
)
185185
if self._pep639:
186-
component.licenses.update(
187-
dist2licenses_pep639(dist,
188-
self._gather_license_texts,
189-
self._logger))
186+
pep639_licenses = list(dist2licenses_pep639(dist, self._gather_license_texts, self._logger))
187+
pep639_lexp = find_LicenseExpression(pep639_licenses)
188+
if pep639_lexp is not None:
189+
component.licenses = (pep639_lexp,) # type:ignore[assignment]
190+
pep639_licenses.remove(pep639_lexp)
191+
if len(pep639_licenses) > 0:
192+
if find_LicenseExpression(component.licenses) is None:
193+
component.licenses.update(pep639_licenses)
194+
else:
195+
# hack for preventing expressions AND named licenses.
196+
# see https://github.com/CycloneDX/cyclonedx-python/issues/826
197+
# see https://github.com/CycloneDX/specification/issues/454
198+
component.evidence = ComponentEvidence(licenses=pep639_licenses)
199+
del pep639_lexp, pep639_licenses
200+
190201
del dist_meta, dist_name, dist_version
191202
self.__component_add_extref_and_purl(component, packagesource4dist(dist))
192203
all_components[normalize_packagename(component.name)] = (

cyclonedx_py/_internal/utils/cdx.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"""
2222

2323
from re import compile as re_compile
24-
from typing import Any, Dict, Iterable
24+
from typing import Any, Dict, Iterable, Optional
2525

2626
from cyclonedx.builder.this import this_component as lib_component
2727
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
@@ -87,11 +87,17 @@ def make_bom(**kwargs: Any) -> Bom:
8787
return bom
8888

8989

90-
def licenses_fixup(licenses: Iterable['License']) -> Iterable['License']:
91-
licenses = set(licenses)
90+
def find_LicenseExpression(licenses: Iterable['License']) -> Optional[LicenseExpression]: # noqa: N802
9291
for license in licenses:
9392
if isinstance(license, LicenseExpression):
94-
return (license,)
93+
return license
94+
return None
95+
96+
97+
def licenses_fixup(licenses: Iterable['License']) -> Iterable['License']:
98+
licenses = set(licenses)
99+
if (lexp := find_LicenseExpression(licenses)) is not None:
100+
return (lexp,)
95101
return licenses
96102

97103

cyclonedx_py/_internal/utils/mimetypes.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,42 @@
1919
from os.path import splitext
2020
from typing import Optional
2121

22+
_MIME_TEXT_PLAIN = 'text/plain'
23+
2224
_MAP_EXT_MIME = {
2325
# https://www.iana.org/assignments/media-types/media-types.xhtml
26+
'.csv': 'text/csv',
27+
'.htm': 'text/html',
28+
'.html': 'text/html',
2429
'.md': 'text/markdown',
2530
'.txt': 'text/plain',
2631
'.rst': 'text/prs.fallenstein.rst',
32+
'.xml': 'text/xml', # not `application/xml` -- our scope is text!
33+
# license-specific files
34+
'.license': _MIME_TEXT_PLAIN,
35+
'.licence': _MIME_TEXT_PLAIN,
2736
# add more mime types. pull-requests welcome!
2837
}
2938

39+
_LICENSE_FNAME_BASE = ('licence', 'license')
40+
_LICENSE_FNAME_EXT = (
41+
'.apache',
42+
'.bsd',
43+
'.gpl',
44+
'.mit',
45+
)
46+
3047

3148
def guess_type(file_name: str) -> Optional[str]:
3249
"""
3350
The stdlib `mimetypes.guess_type()` is inconsistent, as it depends heavily on type registry in the env/os.
3451
Therefore, this polyfill exists.
3552
"""
36-
ext = splitext(file_name)[1].lower()
37-
return _MAP_EXT_MIME.get(
38-
ext,
39-
_stdlib_guess_type(file_name)[0]
40-
)
53+
file_name_l = file_name.lower()
54+
base, ext = splitext(file_name_l)
55+
if ext == '':
56+
return None
57+
if base in _LICENSE_FNAME_BASE and ext in _LICENSE_FNAME_EXT:
58+
return _MIME_TEXT_PLAIN
59+
return _MAP_EXT_MIME.get(ext) \
60+
or _stdlib_guess_type(file_name_l)[0]

tests/_data/infiles/environment/with-license-pep639/init.py

+3
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ def main() -> None:
6363
).create(env_dir)
6464

6565
pip_install(
66+
'--no-dependencies',
6667
# with License-Expression
6768
'attrs',
6869
# with License-File
6970
'boolean.py',
7071
'jsonpointer',
7172
'license_expression',
7273
'lxml',
74+
# with expression-like License AND License-File
75+
'cryptography==43.0.1', # https://github.com/CycloneDX/cyclonedx-python/issues/826
7376
)
7477

7578

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
attrs==23.2.0
22
boolean.py==4.0
3+
cryptography==43.0.1
34
jsonpointer==2.4
45
license-expression==30.3.0
56
lxml==5.3.0

tests/_data/infiles/environment/with-license-pep639/pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ dependencies = [
1212
"jsonpointer",
1313
"license_expression",
1414
"lxml",
15+
# with expression-like License AND License-File
16+
"cryptography",
1517
]

tests/_data/snapshots/environment/pep639-texts_with-license-pep639_1.0.xml.bin

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/pep639-texts_with-license-pep639_1.1.xml.bin

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/pep639-texts_with-license-pep639_1.2.json.bin

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/pep639-texts_with-license-pep639_1.2.xml.bin

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)