Skip to content

Commit e490429

Browse files
authored
feat: model.XsUri migrate control characters according to spec (#498)
fixes #497 --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 78957e6 commit e490429

12 files changed

+409
-6
lines changed

cyclonedx/model/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import re
1717
from datetime import datetime, timezone
1818
from enum import Enum
19+
from functools import reduce
1920
from hashlib import sha1
2021
from itertools import zip_longest
2122
from typing import Any, Iterable, Optional, Tuple, TypeVar
@@ -424,16 +425,47 @@ class XsUri(serializable.helpers.BaseHelper):
424425
425426
.. note::
426427
See XSD definition for xsd:anyURI: http://www.datypic.com/sc/xsd/t-xsd_anyURI.html
428+
See JSON Schema definition for iri-reference: https://tools.ietf.org/html/rfc3987
427429
"""
428430

429431
_INVALID_URI_REGEX = re.compile(r'%(?![0-9A-F]{2})|#.*#', re.IGNORECASE + re.MULTILINE)
430432

433+
__SPEC_REPLACEMENTS = (
434+
(' ', '%20'),
435+
('[', '%5B'),
436+
(']', '%5D'),
437+
('<', '%3C'),
438+
('>', '%3E'),
439+
('{', '%7B'),
440+
('}', '%7D'),
441+
)
442+
443+
@staticmethod
444+
def __spec_replace(v: str, r: Tuple[str, str]) -> str:
445+
return v.replace(*r)
446+
447+
@classmethod
448+
def _spec_migrate(cls, o: str) -> str:
449+
"""
450+
Make a string valid to
451+
- XML::anyURI spec.
452+
- JSON::iri-reference spec.
453+
454+
BEST EFFORT IMPLEMENTATION
455+
456+
@see http://www.w3.org/TR/xmlschema-2/#anyURI
457+
@see http://www.datypic.com/sc/xsd/t-xsd_anyURI.html
458+
@see https://datatracker.ietf.org/doc/html/rfc2396
459+
@see https://datatracker.ietf.org/doc/html/rfc3987
460+
"""
461+
return reduce(cls.__spec_replace, cls.__SPEC_REPLACEMENTS, o)
462+
431463
def __init__(self, uri: str) -> None:
432464
if re.search(XsUri._INVALID_URI_REGEX, uri):
433465
raise InvalidUriException(
434466
f"Supplied value '{uri}' does not appear to be a valid URI."
435467
)
436-
self._uri = uri
468+
self._uri = self._spec_migrate(uri)
437469

438470
@property
439471
@serializable.json_name('.')

tests/_data/models.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,31 @@ def get_bom_with_multiple_licenses() -> Bom:
754754
)
755755

756756

757+
def get_bom_for_issue_497_urls() -> Bom:
758+
"""regression test for issue #497
759+
see https://github.com/CycloneDX/cyclonedx-python-lib/issues/497
760+
"""
761+
return _make_bom(components=[
762+
Component(name='dummy', bom_ref='dummy', external_references=[
763+
ExternalReference(
764+
type=ExternalReferenceType.OTHER,
765+
comment='nothing special',
766+
url=XsUri('https://acme.org')
767+
),
768+
ExternalReference(
769+
type=ExternalReferenceType.OTHER,
770+
comment='control characters',
771+
url=XsUri('https://acme.org/?foo=sp ace&bar[23]=42&lt=1<2&gt=3>2&cb={lol}')
772+
),
773+
ExternalReference(
774+
type=ExternalReferenceType.OTHER,
775+
comment='pre-encoded',
776+
url=XsUri('https://acme.org/?bar%5b23%5D=42')
777+
),
778+
])
779+
])
780+
781+
757782
def bom_all_same_bomref() -> Tuple[Bom, int]:
758783
bom = Bom()
759784
bom.metadata.component = Component(name='root', bom_ref='foo', components=[
@@ -774,13 +799,18 @@ def bom_all_same_bomref() -> Tuple[Bom, int]:
774799
if n.startswith('get_bom_') and not n.endswith('_invalid')
775800
)
776801

802+
all_get_bom_funct_valid_immut = tuple(
803+
(n, f) for n, f in getmembers(sys.modules[__name__], isfunction)
804+
if n.startswith('get_bom_') and not n.endswith('_invalid') and not n.endswith('_migrate')
805+
)
806+
777807
all_get_bom_funct_invalid = tuple(
778808
(n, f) for n, f in getmembers(sys.modules[__name__], isfunction)
779809
if n.startswith('get_bom_') and n.endswith('_invalid')
780810
)
781811

782812
all_get_bom_funct_with_incomplete_deps = {
783-
# List of functions that return BOM with an incomplte dependency graph.
813+
# List of functions that return BOM with an incomplete dependency graph.
784814
# It is expected that some process auto-fixes this before actual serialization takes place.
785815
get_bom_just_complete_metadata,
786816
get_bom_with_component_setuptools_basic,
@@ -797,4 +827,5 @@ def bom_all_same_bomref() -> Tuple[Bom, int]:
797827
get_bom_with_services_simple,
798828
get_bom_with_licenses,
799829
get_bom_with_multiple_licenses,
830+
get_bom_for_issue_497_urls,
800831
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
3+
<components>
4+
<component type="library">
5+
<name>dummy</name>
6+
<version/>
7+
<modified>false</modified>
8+
</component>
9+
</components>
10+
</bom>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<components>
4+
<component type="library" bom-ref="dummy">
5+
<name>dummy</name>
6+
<version/>
7+
<externalReferences>
8+
<reference type="other">
9+
<url>https://acme.org</url>
10+
<comment>nothing special</comment>
11+
</reference>
12+
<reference type="other">
13+
<url>https://acme.org/?bar%5b23%5D=42</url>
14+
<comment>pre-encoded</comment>
15+
</reference>
16+
<reference type="other">
17+
<url>https://acme.org/?foo=sp%20ace&amp;bar%5B23%5D=42&amp;lt=1%3C2&amp;gt=3%3E2&amp;cb=%7Blol%7D</url>
18+
<comment>control characters</comment>
19+
</reference>
20+
</externalReferences>
21+
</component>
22+
</components>
23+
</bom>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"components": [
3+
{
4+
"bom-ref": "dummy",
5+
"externalReferences": [
6+
{
7+
"comment": "nothing special",
8+
"type": "other",
9+
"url": "https://acme.org"
10+
},
11+
{
12+
"comment": "pre-encoded",
13+
"type": "other",
14+
"url": "https://acme.org/?bar%5b23%5D=42"
15+
},
16+
{
17+
"comment": "control characters",
18+
"type": "other",
19+
"url": "https://acme.org/?foo=sp%20ace&bar%5B23%5D=42&lt=1%3C2&gt=3%3E2&cb=%7Blol%7D"
20+
}
21+
],
22+
"name": "dummy",
23+
"type": "library",
24+
"version": ""
25+
}
26+
],
27+
"dependencies": [
28+
{
29+
"ref": "dummy"
30+
}
31+
],
32+
"metadata": {
33+
"timestamp": "2023-01-07T13:44:32.312678+00:00",
34+
"tools": [
35+
{
36+
"name": "cyclonedx-python-lib",
37+
"vendor": "CycloneDX",
38+
"version": "TESTING"
39+
}
40+
]
41+
},
42+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
43+
"version": 1,
44+
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
45+
"bomFormat": "CycloneDX",
46+
"specVersion": "1.2"
47+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>TESTING</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components>
14+
<component type="library" bom-ref="dummy">
15+
<name>dummy</name>
16+
<version/>
17+
<externalReferences>
18+
<reference type="other">
19+
<url>https://acme.org</url>
20+
<comment>nothing special</comment>
21+
</reference>
22+
<reference type="other">
23+
<url>https://acme.org/?bar%5b23%5D=42</url>
24+
<comment>pre-encoded</comment>
25+
</reference>
26+
<reference type="other">
27+
<url>https://acme.org/?foo=sp%20ace&amp;bar%5B23%5D=42&amp;lt=1%3C2&amp;gt=3%3E2&amp;cb=%7Blol%7D</url>
28+
<comment>control characters</comment>
29+
</reference>
30+
</externalReferences>
31+
</component>
32+
</components>
33+
<dependencies>
34+
<dependency ref="dummy"/>
35+
</dependencies>
36+
</bom>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"components": [
3+
{
4+
"bom-ref": "dummy",
5+
"externalReferences": [
6+
{
7+
"comment": "nothing special",
8+
"type": "other",
9+
"url": "https://acme.org"
10+
},
11+
{
12+
"comment": "pre-encoded",
13+
"type": "other",
14+
"url": "https://acme.org/?bar%5b23%5D=42"
15+
},
16+
{
17+
"comment": "control characters",
18+
"type": "other",
19+
"url": "https://acme.org/?foo=sp%20ace&bar%5B23%5D=42&lt=1%3C2&gt=3%3E2&cb=%7Blol%7D"
20+
}
21+
],
22+
"name": "dummy",
23+
"type": "library",
24+
"version": ""
25+
}
26+
],
27+
"dependencies": [
28+
{
29+
"ref": "dummy"
30+
}
31+
],
32+
"metadata": {
33+
"timestamp": "2023-01-07T13:44:32.312678+00:00",
34+
"tools": [
35+
{
36+
"name": "cyclonedx-python-lib",
37+
"vendor": "CycloneDX",
38+
"version": "TESTING"
39+
}
40+
]
41+
},
42+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
43+
"version": 1,
44+
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
45+
"bomFormat": "CycloneDX",
46+
"specVersion": "1.3"
47+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>TESTING</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components>
14+
<component type="library" bom-ref="dummy">
15+
<name>dummy</name>
16+
<version/>
17+
<externalReferences>
18+
<reference type="other">
19+
<url>https://acme.org</url>
20+
<comment>nothing special</comment>
21+
</reference>
22+
<reference type="other">
23+
<url>https://acme.org/?bar%5b23%5D=42</url>
24+
<comment>pre-encoded</comment>
25+
</reference>
26+
<reference type="other">
27+
<url>https://acme.org/?foo=sp%20ace&amp;bar%5B23%5D=42&amp;lt=1%3C2&amp;gt=3%3E2&amp;cb=%7Blol%7D</url>
28+
<comment>control characters</comment>
29+
</reference>
30+
</externalReferences>
31+
</component>
32+
</components>
33+
<dependencies>
34+
<dependency ref="dummy"/>
35+
</dependencies>
36+
</bom>

0 commit comments

Comments
 (0)