Skip to content

Commit 8f99d4e

Browse files
committed
refactor: make bomreader a factory
By making a the BomReader a factory, we can unify the SBOM type detection based on the input channel (e.g. file, stream, json). By that, this complex and inconsistently implemented logic can be re-used across the project. This commit only provides the factory and does some minimal refactorings to make the tests pass. In later commits more use of the infrastructure will be made. Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com>
1 parent 5319f5e commit 8f99d4e

File tree

7 files changed

+140
-74
lines changed

7 files changed

+140
-74
lines changed

src/debsbom/bomreader/bomreader.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,67 @@
33
# SPDX-License-Identifier: MIT
44

55
from abc import abstractmethod
6+
from io import IOBase
67
from pathlib import Path
7-
from io import TextIOBase
88

9+
from ..sbom import SBOMType
10+
from ..util.sbom_processor import SbomProcessor
911

10-
class BomReader:
12+
13+
class BomReader(SbomProcessor):
1114
"""Base class for SBOM importers"""
1215

1316
@classmethod
14-
@abstractmethod
15-
def read_file(cls, filename: Path):
16-
"""Parse and return a BOM instance from the file"""
17-
raise NotImplementedError()
17+
def create(cls, filename: Path, bomtype: SBOMType | None = None):
18+
if bomtype is SBOMType.SPDX or filename.name.endswith("spdx.json"):
19+
SBOMType.SPDX.validate_dependency_availability()
20+
from .spdxbomreader import SpdxBomFileReader
21+
22+
reader_cls = SpdxBomFileReader
23+
elif bomtype is SBOMType.CycloneDX or filename.name.endswith("cdx.json"):
24+
SBOMType.CycloneDX.validate_dependency_availability()
25+
from .cdxbomreader import CdxBomFileReader
26+
27+
reader_cls = CdxBomFileReader
28+
else:
29+
raise RuntimeError("SBOM type cannot be detected based on filename")
30+
31+
return reader_cls(filename)
1832

1933
@classmethod
20-
@abstractmethod
21-
def read_stream(cls, stream: TextIOBase):
22-
"""Parse and return a BOM instance from the stream"""
23-
raise NotImplementedError()
34+
def from_stream(cls, stream: IOBase, bomtype: SBOMType):
35+
if bomtype is SBOMType.SPDX:
36+
SBOMType.SPDX.validate_dependency_availability()
37+
from .spdxbomreader import SpdxBomFileReader
38+
39+
reader_cls = SpdxBomFileReader
40+
elif bomtype is SBOMType.CycloneDX:
41+
SBOMType.CycloneDX.validate_dependency_availability()
42+
from .cdxbomreader import CdxBomFileReader
43+
44+
reader_cls = CdxBomFileReader
45+
else:
46+
raise NotImplementedError("Unsupported SBOM type")
47+
48+
return reader_cls(stream)
2449

2550
@classmethod
51+
def from_json(cls, json_obj, bomtype: SBOMType):
52+
if bomtype is SBOMType.SPDX:
53+
SBOMType.SPDX.validate_dependency_availability()
54+
from .spdxbomreader import SpdxBomJsonReader
55+
56+
reader_cls = SpdxBomJsonReader
57+
elif bomtype is SBOMType.CycloneDX:
58+
SBOMType.CycloneDX.validate_dependency_availability()
59+
from .cdxbomreader import CdxBomJsonReader
60+
61+
reader_cls = CdxBomJsonReader
62+
else:
63+
raise NotImplementedError("Unsupported SBOM type")
64+
65+
return reader_cls(json_obj)
66+
2667
@abstractmethod
27-
def from_json(cls, json_obj):
28-
"""Parse and return a BOM instance from a Json object"""
29-
raise NotImplementedError
68+
def read(self):
69+
return NotImplementedError()

src/debsbom/bomreader/cdxbomreader.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,32 @@
1212
from cyclonedx.model.bom import Bom
1313

1414

15-
class CdxBomReader(BomReader, CDXType):
16-
"""Import an CycloneDX SBOM"""
15+
class CdxBomFileReader(BomReader, CDXType):
16+
"""Import a CycloneDX SBOM from a file"""
1717

18-
@classmethod
19-
def read_file(cls, filename: Path) -> Bom:
20-
with open(filename, "r") as f:
21-
return cls.read_stream(f)
18+
def __init__(self, filename: Path):
19+
self.filename = filename
2220

23-
@classmethod
24-
def read_stream(cls, stream: TextIOBase) -> Bom:
25-
return cls.from_json(json.load(stream))
21+
def read(self) -> Bom:
22+
with open(self.filename, "r") as f:
23+
return CdxBomStreamReader(f).read()
2624

27-
@classmethod
28-
def from_json(cls, json_obj) -> Bom:
29-
return Bom.from_json(json_obj)
25+
26+
class CdxBomStreamReader(BomReader, CDXType):
27+
"""Import a CycloneDX SBOM from a file stream"""
28+
29+
def __init__(self, stream: TextIOBase):
30+
self.stream = stream
31+
32+
def read(self) -> Bom:
33+
return CdxBomJsonReader(json.load(self.stream)).read()
34+
35+
36+
class CdxBomJsonReader(BomReader, CDXType):
37+
"""Import a CycloneDX SBOM from a json object"""
38+
39+
def __init__(self, json_obj):
40+
self.json_obj = json_obj
41+
42+
def read(self) -> Bom:
43+
return Bom.from_json(self.json_obj)

src/debsbom/bomreader/spdxbomreader.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from pathlib import Path
77
from io import TextIOBase
88

9-
from pathlib import Path
109
from spdx_tools.spdx.parser.parse_anything import parse_file as spdx_parse_file
1110
from spdx_tools.spdx.parser.jsonlikedict.json_like_dict_parser import JsonLikeDictParser
1211
from spdx_tools.spdx.model.document import Document
@@ -15,17 +14,31 @@
1514
from ..sbom import SPDXType
1615

1716

18-
class SpdxBomReader(BomReader, SPDXType):
19-
"""Import an SPDX SBOM"""
17+
class SpdxBomFileReader(BomReader, SPDXType):
18+
"""Import a CycloneDX SBOM from a file"""
19+
20+
def __init__(self, filename: Path):
21+
self.filename = filename
22+
23+
def read(self) -> Document:
24+
return spdx_parse_file(str(self.filename))
25+
26+
27+
class SpdxBomStreamReader(BomReader, SPDXType):
28+
"""Import a CycloneDX SBOM from a file stream"""
29+
30+
def __init__(self, stream: TextIOBase):
31+
self.stream = stream
32+
33+
def read(self) -> Document:
34+
return SpdxBomJsonReader(json.load(self.stream)).read()
35+
2036

21-
@classmethod
22-
def read_file(cls, filename: Path) -> Document:
23-
return spdx_parse_file(str(filename))
37+
class SpdxBomJsonReader(BomReader, SPDXType):
38+
"""Import a CycloneDX SBOM from a json object"""
2439

25-
@classmethod
26-
def read_stream(cls, stream: TextIOBase) -> Document:
27-
return cls.from_json(json.load(stream))
40+
def __init__(self, json_obj):
41+
self.json_obj = json_obj
2842

29-
@classmethod
30-
def from_json(cls, json_obj) -> Document:
31-
return JsonLikeDictParser().parse(json_obj)
43+
def read(self) -> Document:
44+
return JsonLikeDictParser().parse(self.json_obj)

src/debsbom/commands/merge.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ def run(args):
4747
raise ValueError("can not merge mixed SPDX and CycloneDX documents")
4848
elif len(spdx_paths) > 0 or args.sbom_type == "spdx":
4949
SBOMType.SPDX.validate_dependency_availability()
50-
from ..bomreader.spdxbomreader import SpdxBomReader
50+
from ..bomreader.spdxbomreader import SpdxBomFileReader, SpdxBomJsonReader
5151
from ..merge.spdx import SpdxSbomMerger
5252

5353
if json_sboms:
5454
for obj in json_sboms:
55-
docs.append(SpdxBomReader.from_json(obj))
55+
docs.append(SpdxBomJsonReader(obj).read())
5656
for path in spdx_paths:
57-
docs.append(SpdxBomReader.read_file(path))
57+
docs.append(SpdxBomFileReader(path).read())
5858
sbom_merger = SpdxSbomMerger(
5959
distro_name=args.distro_name,
6060
distro_supplier=args.distro_supplier,
@@ -74,14 +74,14 @@ def run(args):
7474
BomWriter.write_to_file(bom, SBOMType.SPDX, Path(out), args.validate)
7575
elif len(cdx_paths) > 0 or args.sbom_type == "cdx":
7676
SBOMType.CycloneDX.validate_dependency_availability()
77-
from ..bomreader.cdxbomreader import CdxBomReader
77+
from ..bomreader.cdxbomreader import CdxBomFileReader, CdxBomJsonReader
7878
from ..merge.cdx import CdxSbomMerger
7979

8080
if json_sboms:
8181
for obj in json_sboms:
82-
docs.append(CdxBomReader.from_json(obj))
82+
docs.append(CdxBomJsonReader(obj).read())
8383
for path in cdx_paths:
84-
docs.append(CdxBomReader.read_file(path))
84+
docs.append(CdxBomFileReader(path).read())
8585
sbom_merger = CdxSbomMerger(
8686
distro_name=args.distro_name,
8787
distro_supplier=args.distro_supplier,

src/debsbom/export/exporter.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88
from io import IOBase
99

10+
from ..bomreader.bomreader import BomReader
1011
from ..util.sbom_processor import SbomProcessor
1112
from ..sbom import SBOMType
1213

@@ -33,24 +34,22 @@ def create(filename: Path, format: GraphOutputFormat) -> "GraphExporter":
3334
"""
3435
Factory to create a GraphExporter for the given SBOM type (based on the filename extension).
3536
"""
36-
if filename.name.endswith("spdx.json"):
37-
SBOMType.SPDX.validate_dependency_availability()
38-
from ..bomreader.spdxbomreader import SpdxBomReader
39-
from .spdx import SpdxGraphMLExporter
37+
reader = BomReader.create(filename)
38+
if format == GraphOutputFormat.GRAPHML:
39+
if reader.sbom_type() == SBOMType.SPDX:
40+
from .spdx import SpdxGraphMLExporter
4041

41-
bom = SpdxBomReader.read_file(filename)
42-
if format == GraphOutputFormat.GRAPHML:
43-
return SpdxGraphMLExporter(bom)
44-
elif filename.name.endswith("cdx.json"):
45-
SBOMType.CycloneDX.validate_dependency_availability()
46-
from ..bomreader.cdxbomreader import CdxBomReader
47-
from .cdx import CdxGraphMLExporter
42+
exporter_cls = SpdxGraphMLExporter
43+
elif reader.sbom_type() == SBOMType.CycloneDX:
44+
from .cdx import CdxGraphMLExporter
4845

49-
bom = CdxBomReader.read_file(filename)
50-
if format == GraphOutputFormat.GRAPHML:
51-
return CdxGraphMLExporter(bom)
46+
exporter_cls = CdxGraphMLExporter
47+
else:
48+
raise NotImplementedError("unreachable")
5249
else:
53-
raise RuntimeError("Cannot determine file format")
50+
raise NotImplementedError("unreachable")
51+
52+
return exporter_cls(reader.read())
5453

5554
@staticmethod
5655
def from_stream(

src/debsbom/resolver/resolver.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ def create(filename: Path, bomtype: SBOMType | None = None) -> "PackageResolver"
3434
if filename.name.endswith("spdx.json"):
3535
SBOMType.SPDX.validate_dependency_availability()
3636
from .spdx import SpdxPackageResolver
37-
from ..bomreader.spdxbomreader import SpdxBomReader
37+
from ..bomreader.spdxbomreader import SpdxBomFileReader
3838

39-
return SpdxPackageResolver(SpdxBomReader.read_file(filename))
39+
return SpdxPackageResolver(SpdxBomFileReader(filename).read())
4040
elif filename.name.endswith("cdx.json"):
4141
SBOMType.CycloneDX.validate_dependency_availability()
4242
from .cdx import CdxPackageResolver
43-
from ..bomreader.cdxbomreader import CdxBomReader
43+
from ..bomreader.cdxbomreader import CdxBomFileReader
4444

45-
return CdxPackageResolver(CdxBomReader.read_file(filename))
45+
return CdxPackageResolver(CdxBomFileReader(filename).read())
4646
else:
4747
raise RuntimeError("Cannot determine file format")
4848

tests/test_merge.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def test_spdx_merge():
1212
_spdx_tools = pytest.importorskip("spdx_tools")
1313

1414
from spdx_tools.spdx.model.relationship import Relationship, RelationshipType
15-
from debsbom.bomreader.spdxbomreader import SpdxBomReader
15+
from debsbom.bomreader.spdxbomreader import SpdxBomFileReader
1616
from debsbom.merge.spdx import SpdxSbomMerger
1717

1818
# The test files are created with
@@ -24,7 +24,7 @@ def test_spdx_merge():
2424
merger = SpdxSbomMerger(distro_name=distro_name)
2525
docs = []
2626
for sbom in ["tests/data/merge-full.spdx.json", "tests/data/merge-minimal.spdx.json"]:
27-
docs.append(SpdxBomReader.read_file(Path(sbom)))
27+
docs.append(SpdxBomFileReader(Path(sbom)).read())
2828
bom = merger.merge(docs)
2929

3030
assert (
@@ -73,7 +73,7 @@ def test_cdx_merge():
7373
_cyclonedx = pytest.importorskip("cyclonedx")
7474

7575
from cyclonedx.model.dependency import Dependency
76-
from debsbom.bomreader.cdxbomreader import CdxBomReader
76+
from debsbom.bomreader.cdxbomreader import CdxBomFileReader
7777
from debsbom.merge.cdx import CdxSbomMerger
7878

7979
# The test files are created with
@@ -85,7 +85,7 @@ def test_cdx_merge():
8585
merger = CdxSbomMerger(distro_name=distro_name)
8686
docs = []
8787
for sbom in ["tests/data/merge-full.cdx.json", "tests/data/merge-minimal.cdx.json"]:
88-
docs.append(CdxBomReader.read_file(Path(sbom)))
88+
docs.append(CdxBomFileReader(Path(sbom)).read())
8989
bom = merger.merge(docs)
9090

9191
distro_bom_ref = bom.metadata.component.bom_ref
@@ -121,7 +121,7 @@ def test_cdx_merge():
121121
def test_cdx_hash_merge():
122122
_cyclonedx = pytest.importorskip("cyclonedx")
123123

124-
from debsbom.bomreader.cdxbomreader import CdxBomReader
124+
from debsbom.bomreader.cdxbomreader import CdxBomFileReader
125125
from debsbom.merge.cdx import CdxSbomMerger
126126

127127
distro_name = "cdx-merge-hash-merge"
@@ -131,7 +131,7 @@ def test_cdx_hash_merge():
131131
"tests/data/checksum-merge-md5.cdx.json",
132132
"tests/data/checksum-merge-sha256.cdx.json",
133133
]:
134-
docs.append(CdxBomReader.read_file(Path(sbom)))
134+
docs.append(CdxBomFileReader(Path(sbom)).read())
135135
bom = merger.merge(docs)
136136

137137
component = next(iter(bom.components))
@@ -141,7 +141,7 @@ def test_cdx_hash_merge():
141141
def test_spdx_checksum_merge():
142142
_spdx_tools = pytest.importorskip("spdx_tools")
143143

144-
from debsbom.bomreader.spdxbomreader import SpdxBomReader
144+
from debsbom.bomreader.spdxbomreader import SpdxBomFileReader
145145
from debsbom.merge.spdx import SpdxSbomMerger
146146

147147
distro_name = "spx-merge-checksum-merge"
@@ -151,7 +151,7 @@ def test_spdx_checksum_merge():
151151
"tests/data/checksum-merge-md5.spdx.json",
152152
"tests/data/checksum-merge-sha256.spdx.json",
153153
]:
154-
docs.append(SpdxBomReader.read_file(Path(sbom)))
154+
docs.append(SpdxBomFileReader(Path(sbom)).read())
155155
bom = merger.merge(docs)
156156

157157
package = next(iter(filter(lambda p: p.name == "example-pkg", bom.packages)))
@@ -161,7 +161,7 @@ def test_spdx_checksum_merge():
161161
def test_cdx_bad_checksum():
162162
_cyclonedx = pytest.importorskip("cyclonedx")
163163

164-
from debsbom.bomreader.cdxbomreader import CdxBomReader
164+
from debsbom.bomreader.cdxbomreader import CdxBomFileReader
165165
from debsbom.merge.cdx import CdxSbomMerger
166166

167167
distro_name = "cdx-merge-hash-merge"
@@ -171,15 +171,15 @@ def test_cdx_bad_checksum():
171171
"tests/data/checksum-merge-md5.cdx.json",
172172
"tests/data/checksum-merge-bad.cdx.json",
173173
]:
174-
docs.append(CdxBomReader.read_file(Path(sbom)))
174+
docs.append(CdxBomFileReader(Path(sbom)).read())
175175
with pytest.raises(ChecksumMismatchError):
176176
_bom = merger.merge(docs)
177177

178178

179179
def test_spdx_bad_checksum():
180180
_spdx_tools = pytest.importorskip("spdx_tools")
181181

182-
from debsbom.bomreader.spdxbomreader import SpdxBomReader
182+
from debsbom.bomreader.spdxbomreader import SpdxBomFileReader
183183
from debsbom.merge.spdx import SpdxSbomMerger
184184

185185
distro_name = "cdx-merge-checksum-bad"
@@ -189,7 +189,7 @@ def test_spdx_bad_checksum():
189189
"tests/data/checksum-merge-md5.spdx.json",
190190
"tests/data/checksum-merge-bad.spdx.json",
191191
]:
192-
docs.append(SpdxBomReader.read_file(Path(sbom)))
192+
docs.append(SpdxBomFileReader(Path(sbom)).read())
193193

194194
with pytest.raises(ChecksumMismatchError):
195195
_bom = merger.merge(docs)

0 commit comments

Comments
 (0)