Skip to content

Commit ee80ea3

Browse files
committed
properly added .component.swhid
Signed-off-by: Paul Horton <[email protected]>
1 parent 875a338 commit ee80ea3

File tree

5 files changed

+115
-23
lines changed

5 files changed

+115
-23
lines changed

cyclonedx/exception/model.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ class InvalidOmniBorIdException(CycloneDxModelException):
5353
pass
5454

5555

56+
class InvalidSwhidException(CycloneDxModelException):
57+
"""
58+
Raised when a supplied value for an Swhid does not meet the format requirements
59+
as defined at https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html.
60+
"""
61+
pass
62+
63+
5664
class InvalidUriException(CycloneDxModelException):
5765
"""
5866
Raised when a `str` is provided that needs to be a valid URI, but isn't.

cyclonedx/model/component.py

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from .._internal.compare import ComparableTuple as _ComparableTuple
2929
from .._internal.hash import file_sha1sum as _file_sha1sum
30-
from ..exception.model import InvalidOmniBorIdException, NoPropertiesProvidedException
30+
from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException, NoPropertiesProvidedException
3131
from ..exception.serialization import (
3232
CycloneDxDeserializationException,
3333
SerializationOfUnexpectedValueException,
@@ -932,6 +932,65 @@ def __str__(self) -> str:
932932
return self._id
933933

934934

935+
@serializable.serializable_class
936+
class Swhid(serializable.helpers.BaseHelper):
937+
"""
938+
Helper class that allows us to perform validation on data strings that must conform to
939+
https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html.
940+
941+
"""
942+
943+
_VALID_SWHID_REGEX = re.compile(r'^swh:1:(cnp|rel|rev|dir|cnt):([0-9a-z]{40})(.*)?$')
944+
945+
def __init__(self, id: str) -> None:
946+
if Swhid._VALID_SWHID_REGEX.match(id) is None:
947+
raise InvalidSwhidException(
948+
f'Supplied value "{id} does not meet format specification.'
949+
)
950+
self._id = id
951+
952+
@property
953+
@serializable.json_name('.')
954+
@serializable.xml_name('.')
955+
def id(self) -> str:
956+
return self._id
957+
958+
@classmethod
959+
def serialize(cls, o: Any) -> str:
960+
if isinstance(o, Swhid):
961+
return str(o)
962+
raise SerializationOfUnexpectedValueException(
963+
f'Attempt to serialize a non-Swhid: {o!r}')
964+
965+
@classmethod
966+
def deserialize(cls, o: Any) -> 'Swhid':
967+
try:
968+
return Swhid(id=str(o))
969+
except ValueError as err:
970+
raise CycloneDxDeserializationException(
971+
f'Swhid string supplied does not parse: {o!r}'
972+
) from err
973+
974+
def __eq__(self, other: Any) -> bool:
975+
if isinstance(other, Swhid):
976+
return hash(other) == hash(self)
977+
return False
978+
979+
def __lt__(self, other: Any) -> bool:
980+
if isinstance(other, Swhid):
981+
return self._id < other._id
982+
return NotImplemented
983+
984+
def __hash__(self) -> int:
985+
return hash(self._id)
986+
987+
def __repr__(self) -> str:
988+
return f'<Swhid {self._id}>'
989+
990+
def __str__(self) -> str:
991+
return self._id
992+
993+
935994
@serializable.serializable_class
936995
class Component(Dependable):
937996
"""
@@ -986,7 +1045,7 @@ def __init__(self, *,
9861045
modified: bool = False, manufacturer: Optional[OrganizationalEntity] = None,
9871046
authors: Optional[Iterable[OrganizationalContact]] = None,
9881047
omnibor_ids: Optional[Iterable[OmniborId]] = None,
989-
# swhid: Optional[Iterable[str]] = None,
1048+
swhids: Optional[Iterable[Swhid]] = None,
9901049
# Deprecated in v1.6
9911050
author: Optional[str] = None,
9921051
) -> None:
@@ -1011,7 +1070,7 @@ def __init__(self, *,
10111070
self.cpe = cpe
10121071
self.purl = purl
10131072
self.omnibor_ids = omnibor_ids or [] # type:ignore[assignment]
1014-
# self.swhid = swhid or [] # type:ignore[assignment]
1073+
self.swhids = swhids or [] # type:ignore[assignment]
10151074
self.swid = swid
10161075
self.modified = modified
10171076
self.pedigree = pedigree
@@ -1380,25 +1439,26 @@ def omnibor_ids(self) -> 'SortedSet[OmniborId]':
13801439
def omnibor_ids(self, omnibor_ids: Iterable[OmniborId]) -> None:
13811440
self._omnibor_ids = SortedSet(omnibor_ids)
13821441

1383-
# @property
1384-
# @serializable.view(SchemaVersion1Dot6)
1385-
# @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='swhid')
1386-
# @serializable.xml_sequence(17)
1387-
# def swhid(self) -> SortedSet[str]:
1388-
# """
1389-
# Specifies the Software Heritage persistent identifier (SWHID). The SWHID, if specified, MUST be valid and
1390-
# conform to the specification defined at:
1391-
# https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html
1392-
#
1393-
# Returns:
1394-
# `Iterable[swhid]` if set else `None`
1395-
# """
1396-
# return self._swhid
1397-
1398-
# @swhid.setter
1399-
# def swhid(self, swhid: Iterable[str]) -> None:
1400-
# self._swhid = SortedSet(swhid)
1401-
#
1442+
@property
1443+
@serializable.json_name('swhid')
1444+
@serializable.view(SchemaVersion1Dot6)
1445+
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='swhid')
1446+
@serializable.xml_sequence(17)
1447+
def swhids(self) -> 'SortedSet[Swhid]':
1448+
"""
1449+
Specifies the Software Heritage persistent identifier (SWHID). The SWHID, if specified, MUST be valid and
1450+
conform to the specification defined at:
1451+
https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html
1452+
1453+
Returns:
1454+
`Iterable[Swhid]` if set else `None`
1455+
"""
1456+
return self._swhids
1457+
1458+
@swhids.setter
1459+
def swhids(self, swhids: Iterable[Swhid]) -> None:
1460+
self._swhids = SortedSet(swhids)
1461+
14021462
@property
14031463
@serializable.view(SchemaVersion1Dot2)
14041464
@serializable.view(SchemaVersion1Dot3)

tests/_data/models.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
Patch,
5757
PatchClassification,
5858
Pedigree,
59+
Swhid,
5960
Swid,
6061
)
6162
from cyclonedx.model.dependency import Dependency
@@ -131,7 +132,20 @@ def get_bom_with_component_setuptools_with_v16_fields() -> Bom:
131132
component.manufacturer = get_org_entity_1()
132133
component.authors = [get_org_contact_1(), get_org_contact_2()]
133134
component.omnibor_ids = [OmniborId('gitoid:blob:sha1:261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64')]
134-
# component.swhid = 'swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2'
135+
component.swhids = [
136+
Swhid('swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2'),
137+
Swhid('swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f'),
138+
Swhid('swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;'
139+
'origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;'
140+
'visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;'
141+
'anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;'
142+
'path=/Examples/SimpleFarm/simplefarm.ml;lines=9-15'),
143+
Swhid('swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;origin=https://github.com/web-platform-tests/wpt;'
144+
'visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;'
145+
'anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;'
146+
'path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/'
147+
'support/x%3Burl=foo/')
148+
]
135149
return _make_bom(components=[component])
136150

137151

tests/_data/snapshots/get_bom_with_component_setuptools_with_v16_fields-1.6.json.bin

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
"gitoid:blob:sha1:261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64"
4545
],
4646
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
47+
"swhid": [
48+
"swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;path=/Examples/SimpleFarm/simplefarm.ml;lines=9-15",
49+
"swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2",
50+
"swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;origin=https://github.com/web-platform-tests/wpt;visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/",
51+
"swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f"
52+
],
4753
"type": "library",
4854
"version": "50.3.2"
4955
}

tests/_data/snapshots/get_bom_with_component_setuptools_with_v16_fields-1.6.xml.bin

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
</licenses>
7474
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
7575
<omniborId>gitoid:blob:sha1:261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64</omniborId>
76+
<swhid>swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;path=/Examples/SimpleFarm/simplefarm.ml;lines=9-15</swhid>
77+
<swhid>swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2</swhid>
78+
<swhid>swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;origin=https://github.com/web-platform-tests/wpt;visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/</swhid>
79+
<swhid>swh:1:rel:22ece559cc7cc2364edc5e5593d63ae8bd229f9f</swhid>
7680
</component>
7781
</components>
7882
<dependencies>

0 commit comments

Comments
 (0)