Skip to content

Commit 548d66e

Browse files
committed
fix: implement __lt__ for models still missing it
Since cyclonedx-python-lib has the tendency to use SortedSets to store deserialized instances, this can lead to errors like these: TypeError: '<' not supported between instances of 'ComponentEvidence' and 'ComponentEvidence' When loading a CycloneDX JSON SBOM using Bom.from_json(). This is due to the lack of 'less-than' operator, which is now implemented with __lt__ for all models that were still missing it. We implemented it this way since that's how it's done for all the existing models, but an easier implementation would propably to add functools' @total_ordering decorator to those classes. Signed-off-by: Quentin Kaiser <quentin.kaiser@onekey.com>
1 parent 0f17348 commit 548d66e

12 files changed

Lines changed: 292 additions & 0 deletions

cyclonedx/model/bom.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ def __eq__(self, other: object) -> bool:
307307
return self.__comparable_tuple() == other.__comparable_tuple()
308308
return False
309309

310+
def __lt__(self, other: object) -> bool:
311+
if isinstance(other, BomMetaData):
312+
return self.__comparable_tuple() == other.__comparable_tuple()
313+
return NotImplemented
314+
310315
def __hash__(self) -> int:
311316
return hash(self.__comparable_tuple())
312317

cyclonedx/model/component.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@ def __eq__(self, other: object) -> bool:
654654
return self.__comparable_tuple() == other.__comparable_tuple()
655655
return False
656656

657+
def __lt__(self, other: object) -> bool:
658+
if isinstance(other, Pedigree):
659+
return self.__comparable_tuple() < other.__comparable_tuple()
660+
return NotImplemented
661+
657662
def __hash__(self) -> int:
658663
return hash(self.__comparable_tuple())
659664

@@ -806,6 +811,11 @@ def __eq__(self, other: object) -> bool:
806811
return self.__comparable_tuple() == other.__comparable_tuple()
807812
return False
808813

814+
def __lt__(self, other: object) -> bool:
815+
if isinstance(other, Swid):
816+
return self.__comparable_tuple() < other.__comparable_tuple()
817+
return NotImplemented
818+
809819
def __hash__(self) -> int:
810820
return hash(self.__comparable_tuple())
811821

cyclonedx/model/component_evidence.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,11 @@ def __eq__(self, other: object) -> bool:
561561
return self.__comparable_tuple() == other.__comparable_tuple()
562562
return False
563563

564+
def __lt__(self, other: object) -> bool:
565+
if isinstance(other, CallStackFrame):
566+
return self.__comparable_tuple() < other.__comparable_tuple()
567+
return NotImplemented
568+
564569
def __hash__(self) -> int:
565570
return hash(self.__comparable_tuple())
566571

@@ -744,6 +749,11 @@ def __eq__(self, other: object) -> bool:
744749
return self.__comparable_tuple() == other.__comparable_tuple()
745750
return False
746751

752+
def __lt__(self, other: object) -> bool:
753+
if isinstance(other, ComponentEvidence):
754+
return self.__comparable_tuple() < other.__comparable_tuple()
755+
return NotImplemented
756+
747757
def __hash__(self) -> int:
748758
return hash(self.__comparable_tuple())
749759

cyclonedx/model/crypto.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,11 @@ def __eq__(self, other: object) -> bool:
507507
return self.__comparable_tuple() == other.__comparable_tuple()
508508
return False
509509

510+
def __lt__(self, other: object) -> bool:
511+
if isinstance(other, AlgorithmProperties):
512+
return self.__comparable_tuple() < other.__comparable_tuple()
513+
return NotImplemented
514+
510515
def __hash__(self) -> int:
511516
return hash(self.__comparable_tuple())
512517

@@ -683,6 +688,11 @@ def __eq__(self, other: object) -> bool:
683688
return self.__comparable_tuple() == other.__comparable_tuple()
684689
return False
685690

691+
def __lt__(self, other: object) -> bool:
692+
if isinstance(other, CertificateProperties):
693+
return self.__comparable_tuple() < other.__comparable_tuple()
694+
return NotImplemented
695+
686696
def __hash__(self) -> int:
687697
return hash(self.__comparable_tuple())
688698

@@ -810,6 +820,11 @@ def __eq__(self, other: object) -> bool:
810820
return self.__comparable_tuple() == other.__comparable_tuple()
811821
return False
812822

823+
def __lt__(self, other: object) -> bool:
824+
if isinstance(other, RelatedCryptoMaterialSecuredBy):
825+
return self.__comparable_tuple() < other.__comparable_tuple()
826+
return NotImplemented
827+
813828
def __hash__(self) -> int:
814829
return hash(self.__comparable_tuple())
815830

@@ -1055,6 +1070,11 @@ def __eq__(self, other: object) -> bool:
10551070
return self.__comparable_tuple() == other.__comparable_tuple()
10561071
return False
10571072

1073+
def __lt__(self, other: object) -> bool:
1074+
if isinstance(other, RelatedCryptoMaterialProperties):
1075+
return self.__comparable_tuple() < other.__comparable_tuple()
1076+
return NotImplemented
1077+
10581078
def __hash__(self) -> int:
10591079
return hash(self.__comparable_tuple())
10601080

@@ -1314,6 +1334,11 @@ def __eq__(self, other: object) -> bool:
13141334
return self.__comparable_tuple() == other.__comparable_tuple()
13151335
return False
13161336

1337+
def __lt__(self, other: object) -> bool:
1338+
if isinstance(other, Ikev2TransformTypes):
1339+
return self.__comparable_tuple() < other.__comparable_tuple()
1340+
return NotImplemented
1341+
13171342
def __hash__(self) -> int:
13181343
return hash(self.__comparable_tuple())
13191344

@@ -1440,6 +1465,11 @@ def __eq__(self, other: object) -> bool:
14401465
return self.__comparable_tuple() == other.__comparable_tuple()
14411466
return False
14421467

1468+
def __lt__(self, other: object) -> bool:
1469+
if isinstance(other, ProtocolProperties):
1470+
return self.__comparable_tuple() < other.__comparable_tuple()
1471+
return NotImplemented
1472+
14431473
def __hash__(self) -> int:
14441474
return hash(self.__comparable_tuple())
14451475

cyclonedx/model/release_note.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ def __eq__(self, other: object) -> bool:
250250
return self.__comparable_tuple() == other.__comparable_tuple()
251251
return False
252252

253+
def __lt__(self, other: object) -> bool:
254+
if isinstance(other, ReleaseNotes):
255+
return self.__comparable_tuple() < other.__comparable_tuple()
256+
return NotImplemented
257+
253258
def __hash__(self) -> int:
254259
return hash(self.__comparable_tuple())
255260

cyclonedx/model/tool.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ def __eq__(self, other: object) -> bool:
266266
return self.__comparable_tuple() == other.__comparable_tuple()
267267
return False
268268

269+
def __lt__(self, other: object) -> bool:
270+
if isinstance(other, ToolRepository):
271+
return self.__comparable_tuple() < other.__comparable_tuple()
272+
return NotImplemented
273+
269274
def __hash__(self) -> int:
270275
return hash(self.__comparable_tuple())
271276

tests/test_model_bom.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ def test_basic_bom_metadata(self) -> None:
107107
self.assertTrue(tools[0] in metadata.tools.tools)
108108
self.assertTrue(tools[1] in metadata.tools.tools)
109109

110+
def test_bom_metadata_sorting(self) -> None:
111+
"""Test that BomMetaData instances can be sorted without triggering TypeError"""
112+
metadata1 = BomMetaData(
113+
tools=[Tool(name='tool_a')],
114+
authors=[OrganizationalContact(name='contact_a')]
115+
)
116+
metadata2 = BomMetaData(
117+
tools=[Tool(name='tool_b')],
118+
authors=[OrganizationalContact(name='contact_b')]
119+
)
120+
metadata3 = BomMetaData(
121+
tools=[Tool(name='tool_c')],
122+
authors=[OrganizationalContact(name='contact_c')]
123+
)
124+
125+
# This should not raise TypeError: '<' not supported between instances
126+
metadata_list = [metadata3, metadata1, metadata2]
127+
sorted_metadata = sorted(metadata_list)
128+
self.assertEqual(len(sorted_metadata), 3)
129+
110130

111131
@ddt
112132
class TestBom(TestCase):

tests/test_model_component.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,17 @@ def test_not_same_1(self) -> None:
419419
self.assertNotEqual(hash(p1), hash(p2), 'hash')
420420
self.assertFalse(p1 == p2, 'equal')
421421

422+
def test_pedigree_sorting(self) -> None:
423+
"""Test that Pedigree instances can be sorted without triggering TypeError"""
424+
p1 = Pedigree(notes='Note A')
425+
p2 = Pedigree(notes='Note B')
426+
p3 = Pedigree(notes='Note C')
427+
428+
# This should not raise TypeError: '<' not supported between instances
429+
pedigree_list = [p3, p1, p2]
430+
sorted_pedigree = sorted(pedigree_list)
431+
self.assertEqual(len(sorted_pedigree), 3)
432+
422433

423434
class TestModelSwid(TestCase):
424435

@@ -442,3 +453,14 @@ def test_not_same(self) -> None:
442453
self.assertNotEqual(id(sw_1), id(sw_2), 'id')
443454
self.assertNotEqual(hash(sw_1), hash(sw_2), 'hash')
444455
self.assertFalse(sw_1 == sw_2, 'equal')
456+
457+
def test_swid_sorting(self) -> None:
458+
"""Test that Swid instances can be sorted without triggering TypeError"""
459+
sw_1 = get_swid_1()
460+
sw_2 = get_swid_2()
461+
sw_3 = get_swid_1()
462+
463+
# This should not raise TypeError: '<' not supported between instances
464+
swid_list = [sw_2, sw_1, sw_3]
465+
sorted_swid = sorted(swid_list)
466+
self.assertEqual(len(sorted_swid), 3)

tests/test_model_component_evidence.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ def test_not_same_1(self) -> None:
201201
self.assertNotEqual(hash(ce_1), hash(ce_2))
202202
self.assertFalse(ce_1 == ce_2)
203203

204+
def test_component_evidence_sorting(self) -> None:
205+
"""Test that ComponentEvidence instances can be sorted without triggering TypeError"""
206+
ce_1 = ComponentEvidence(copyright=[Copyright(text='Copyright A')])
207+
ce_2 = ComponentEvidence(copyright=[Copyright(text='Copyright B')])
208+
ce_3 = ComponentEvidence(copyright=[Copyright(text='Copyright C')])
209+
210+
# This should not raise TypeError: '<' not supported between instances
211+
evidence_list = [ce_3, ce_1, ce_2]
212+
sorted_evidence = sorted(evidence_list)
213+
self.assertEqual(len(sorted_evidence), 3)
214+
204215

205216
class TestModelCallStackFrame(TestCase):
206217

@@ -233,3 +244,14 @@ def test_module_required(self) -> None:
233244
self.assertIsNone(frame.line)
234245
self.assertIsNone(frame.column)
235246
self.assertIsNone(frame.full_filename)
247+
248+
def test_callstack_frame_sorting(self) -> None:
249+
"""Test that CallStackFrame instances can be sorted without triggering TypeError"""
250+
frame1 = CallStackFrame(module='app_a', function='func_a')
251+
frame2 = CallStackFrame(module='app_b', function='func_b')
252+
frame3 = CallStackFrame(module='app_c', function='func_c')
253+
254+
# This should not raise TypeError: '<' not supported between instances
255+
frame_list = [frame3, frame1, frame2]
256+
sorted_frames = sorted(frame_list)
257+
self.assertEqual(len(sorted_frames), 3)

tests/test_model_crypto.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
from unittest import TestCase
19+
20+
from cyclonedx.model.bom_ref import BomRef
21+
from cyclonedx.model.crypto import (
22+
AlgorithmProperties,
23+
CertificateProperties,
24+
CryptoPrimitive,
25+
Ikev2TransformTypes,
26+
ProtocolProperties,
27+
ProtocolPropertiesType,
28+
RelatedCryptoMaterialProperties,
29+
RelatedCryptoMaterialSecuredBy,
30+
RelatedCryptoMaterialType,
31+
)
32+
33+
34+
class TestModelAlgorithmProperties(TestCase):
35+
36+
def test_algorithm_properties_sorting(self) -> None:
37+
"""Test that AlgorithmProperties instances can be sorted without triggering TypeError"""
38+
algo1 = AlgorithmProperties(primitive=CryptoPrimitive.HASH, classical_security_level=128)
39+
algo2 = AlgorithmProperties(primitive=CryptoPrimitive.SIGNATURE, classical_security_level=256)
40+
algo3 = AlgorithmProperties(primitive=CryptoPrimitive.BLOCK_CIPHER, classical_security_level=192)
41+
42+
# This should not raise TypeError: '<' not supported between instances
43+
algo_list = [algo2, algo3, algo1]
44+
sorted_algos = sorted(algo_list)
45+
self.assertEqual(len(sorted_algos), 3)
46+
47+
48+
class TestModelCertificateProperties(TestCase):
49+
50+
def test_certificate_properties_sorting(self) -> None:
51+
"""Test that CertificateProperties instances can be sorted without triggering TypeError"""
52+
cert1 = CertificateProperties(subject_name='CN=Test1', certificate_format='X.509')
53+
cert2 = CertificateProperties(subject_name='CN=Test2', certificate_format='PEM')
54+
cert3 = CertificateProperties(subject_name='CN=Test3', certificate_format='DER')
55+
56+
# This should not raise TypeError: '<' not supported between instances
57+
cert_list = [cert2, cert3, cert1]
58+
sorted_certs = sorted(cert_list)
59+
self.assertEqual(len(sorted_certs), 3)
60+
61+
62+
class TestModelRelatedCryptoMaterialSecuredBy(TestCase):
63+
64+
def test_related_crypto_material_secured_by_sorting(self) -> None:
65+
"""Test that RelatedCryptoMaterialSecuredBy instances can be sorted without triggering TypeError"""
66+
secured1 = RelatedCryptoMaterialSecuredBy(mechanism='HSM', algorithm_ref=BomRef('algo1'))
67+
secured2 = RelatedCryptoMaterialSecuredBy(mechanism='TPM', algorithm_ref=BomRef('algo2'))
68+
secured3 = RelatedCryptoMaterialSecuredBy(mechanism='Software', algorithm_ref=BomRef('algo3'))
69+
70+
# This should not raise TypeError: '<' not supported between instances
71+
secured_list = [secured3, secured1, secured2]
72+
sorted_secured = sorted(secured_list)
73+
self.assertEqual(len(sorted_secured), 3)
74+
75+
76+
class TestModelRelatedCryptoMaterialProperties(TestCase):
77+
78+
def test_related_crypto_material_properties_sorting(self) -> None:
79+
"""Test that RelatedCryptoMaterialProperties instances can be sorted without triggering TypeError"""
80+
material1 = RelatedCryptoMaterialProperties(
81+
type=RelatedCryptoMaterialType.KEY,
82+
id='key1',
83+
size=256
84+
)
85+
material2 = RelatedCryptoMaterialProperties(
86+
type=RelatedCryptoMaterialType.PRIVATE_KEY,
87+
id='key2',
88+
size=512
89+
)
90+
material3 = RelatedCryptoMaterialProperties(
91+
type=RelatedCryptoMaterialType.PUBLIC_KEY,
92+
id='key3',
93+
size=1024
94+
)
95+
96+
# This should not raise TypeError: '<' not supported between instances
97+
material_list = [material3, material1, material2]
98+
sorted_materials = sorted(material_list)
99+
self.assertEqual(len(sorted_materials), 3)
100+
101+
102+
class TestModelIkev2TransformTypes(TestCase):
103+
104+
def test_ikev2_transform_types_sorting(self) -> None:
105+
"""Test that Ikev2TransformTypes instances can be sorted without triggering TypeError"""
106+
ikev2_1 = Ikev2TransformTypes(
107+
encr=[BomRef('encr1')],
108+
esn=True
109+
)
110+
ikev2_2 = Ikev2TransformTypes(
111+
encr=[BomRef('encr2')],
112+
esn=False
113+
)
114+
ikev2_3 = Ikev2TransformTypes(
115+
encr=[BomRef('encr3')],
116+
esn=True
117+
)
118+
119+
# This should not raise TypeError: '<' not supported between instances
120+
ikev2_list = [ikev2_3, ikev2_1, ikev2_2]
121+
sorted_ikev2 = sorted(ikev2_list)
122+
self.assertEqual(len(sorted_ikev2), 3)
123+
124+
125+
class TestModelProtocolProperties(TestCase):
126+
127+
def test_protocol_properties_sorting(self) -> None:
128+
"""Test that ProtocolProperties instances can be sorted without triggering TypeError"""
129+
proto1 = ProtocolProperties(type=ProtocolPropertiesType.TLS, version='1.2')
130+
proto2 = ProtocolProperties(type=ProtocolPropertiesType.SSH, version='2.0')
131+
proto3 = ProtocolProperties(type=ProtocolPropertiesType.IPSEC, version='1.0')
132+
133+
# This should not raise TypeError: '<' not supported between instances
134+
proto_list = [proto3, proto1, proto2]
135+
sorted_protos = sorted(proto_list)
136+
self.assertEqual(len(sorted_protos), 3)

0 commit comments

Comments
 (0)