Skip to content

Commit 780adeb

Browse files
committed
fix: add namespace and subpath support to Component to complete PackageURL Spec support
Signed-off-by: Paul Horton <[email protected]>
1 parent 70689a2 commit 780adeb

File tree

4 files changed

+48
-13
lines changed

4 files changed

+48
-13
lines changed

cyclonedx/model/component.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ class Component:
5252
"""
5353
_type: ComponentType
5454
_package_url_type: str
55+
_namespace: str
5556
_name: str
5657
_version: str
5758
_qualifiers: str
59+
_subpath: str
5860

5961
_author: str = None
6062
_description: str = None
@@ -93,17 +95,21 @@ def for_file(absolute_file_path: str, path_for_bom: str = None):
9395
package_url_type='generic'
9496
)
9597

96-
def __init__(self, name: str, version: str, qualifiers: str = None, hashes: List[HashType] = None,
98+
def __init__(self, name: str, version: str, namespace: str = None, qualifiers: str = None, subpath: str = None,
99+
hashes: List[HashType] = None,
97100
component_type: ComponentType = ComponentType.LIBRARY, package_url_type: str = 'pypi'):
101+
self._package_url_type = package_url_type
102+
self._namespace = namespace
98103
self._name = name
99104
self._version = version
100105
self._type = component_type
101106
self._qualifiers = qualifiers
107+
self._subpath = subpath
108+
102109
self._hashes.clear()
103110
if hashes:
104111
self._hashes = hashes
105112
self._vulnerabilites.clear()
106-
self._package_url_type = package_url_type
107113
self._external_references.clear()
108114

109115
def add_external_reference(self, reference: ExternalReference):
@@ -193,21 +199,36 @@ def get_name(self) -> str:
193199
"""
194200
return self._name
195201

202+
def get_namespace(self) -> str:
203+
"""
204+
Get the namespace of this Component.
205+
206+
Returns:
207+
Declared namespace of this Component as `str` if declared, else `None`.
208+
"""
209+
return self._namespace
210+
196211
def get_purl(self) -> str:
197212
"""
198213
Get the PURL for this Component.
199214
200215
Returns:
201-
PackageURL that reflects this Component as `str`.
216+
PackageURL or 'PURL' that reflects this Component as `str`.
202217
"""
203-
base_purl = 'pkg:{}/{}@{}'.format(self._package_url_type, self._name, self._version)
204-
if self._qualifiers:
205-
base_purl = '{}?{}'.format(base_purl, self._qualifiers)
206-
return base_purl
218+
return self.to_package_url().to_string()
207219

208220
def get_pypi_url(self) -> str:
209221
return f'https://pypi.org/project/{self.get_name()}/{self.get_version()}'
210222

223+
def get_subpath(self) -> str:
224+
"""
225+
Get the subpath of this Component.
226+
227+
Returns:
228+
Declared subpath of this Component as `str` if declared, else `None`.
229+
"""
230+
return self._subpath
231+
211232
def get_type(self) -> ComponentType:
212233
"""
213234
Get the type of this Component.
@@ -292,9 +313,11 @@ def to_package_url(self) -> PackageURL:
292313
"""""
293314
return PackageURL(
294315
type=self._package_url_type,
316+
namespace=self._namespace,
295317
name=self._name,
296318
version=self._version,
297-
qualifiers=self._qualifiers
319+
qualifiers=self._qualifiers,
320+
subpath=self._subpath
298321
)
299322

300323
def __eq__(self, other):

cyclonedx/output/json.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def _get_component_as_dict(self, component: Component) -> dict:
5353
"purl": component.get_purl()
5454
}
5555

56+
if component.get_namespace():
57+
c['group'] = component.get_namespace()
58+
5659
if component.get_hashes():
5760
hashes = []
5861
for component_hash in component.get_hashes():

cyclonedx/output/xml.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
8585
if self.component_supports_author() and component.get_author() is not None:
8686
ElementTree.SubElement(component_element, 'author').text = component.get_author()
8787

88+
# group
89+
if component.get_namespace():
90+
ElementTree.SubElement(component_element, 'group').text = component.get_namespace()
91+
8892
# name
8993
ElementTree.SubElement(component_element, 'name').text = component.get_name()
9094

@@ -94,11 +98,6 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
9498
# hashes
9599
if len(component.get_hashes()) > 0:
96100
Xml._add_hashes_to_element(hashes=component.get_hashes(), element=component_element)
97-
# hashes_e = ElementTree.SubElement(component_element, 'hashes')
98-
# for hash in component.get_hashes():
99-
# ElementTree.SubElement(
100-
# hashes_e, 'hash', {'alg': hash.get_algorithm().value}
101-
# ).text = hash.get_hash_value()
102101

103102
# purl
104103
ElementTree.SubElement(component_element, 'purl').text = component.get_purl()

tests/test_component.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def setUpClass(cls) -> None:
3737
cls._component_generic_file: Component = Component(
3838
name='/test.py', version='UNKNOWN', package_url_type='generic'
3939
)
40+
cls._component_purl_spec_no_subpath: Component = Component(
41+
namespace='name-space', name='setuptools', version='50.0.1', qualifiers='extension=whl'
42+
)
4043

4144
def test_purl_correct(self):
4245
self.assertEqual(
@@ -138,3 +141,10 @@ def test_get_component_by_purl_1(self):
138141
purl=TestComponent._component.get_purl()),
139142
TestComponent._component_2
140143
)
144+
145+
def test_full_purl_spec_no_subpath(self):
146+
self.assertEqual(
147+
TestComponent._component_purl_spec_no_subpath.get_purl(),
148+
'pkg:pypi/name-space/[email protected]?extension=whl'
149+
)
150+
self.assertIsNone(TestComponent._component_purl_spec_no_subpath.get_subpath())

0 commit comments

Comments
 (0)