343 lines
9.2 KiB
Python
343 lines
9.2 KiB
Python
|
#
|
||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||
|
#
|
||
|
|
||
|
#
|
||
|
# This library is intended to capture the JSON SPDX specification in a type
|
||
|
# safe manner. It is not intended to encode any particular OE specific
|
||
|
# behaviors, see the sbom.py for that.
|
||
|
#
|
||
|
# The documented SPDX spec document doesn't cover the JSON syntax for
|
||
|
# particular configuration, which can make it hard to determine what the JSON
|
||
|
# syntax should be. I've found it is actually much simpler to read the official
|
||
|
# SPDX JSON schema which can be found here: https://github.com/spdx/spdx-spec
|
||
|
# in schemas/spdx-schema.json
|
||
|
#
|
||
|
|
||
|
import hashlib
|
||
|
import itertools
|
||
|
import json
|
||
|
|
||
|
SPDX_VERSION = "2.2"
|
||
|
|
||
|
|
||
|
#
|
||
|
# The following are the support classes that are used to implement SPDX object
|
||
|
#
|
||
|
|
||
|
class _Property(object):
|
||
|
"""
|
||
|
A generic SPDX object property. The different types will derive from this
|
||
|
class
|
||
|
"""
|
||
|
|
||
|
def __init__(self, *, default=None):
|
||
|
self.default = default
|
||
|
|
||
|
def setdefault(self, dest, name):
|
||
|
if self.default is not None:
|
||
|
dest.setdefault(name, self.default)
|
||
|
|
||
|
|
||
|
class _String(_Property):
|
||
|
"""
|
||
|
A scalar string property for an SPDX object
|
||
|
"""
|
||
|
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
|
||
|
def set_property(self, attrs, name):
|
||
|
def get_helper(obj):
|
||
|
return obj._spdx[name]
|
||
|
|
||
|
def set_helper(obj, value):
|
||
|
obj._spdx[name] = value
|
||
|
|
||
|
def del_helper(obj):
|
||
|
del obj._spdx[name]
|
||
|
|
||
|
attrs[name] = property(get_helper, set_helper, del_helper)
|
||
|
|
||
|
def init(self, source):
|
||
|
return source
|
||
|
|
||
|
|
||
|
class _Object(_Property):
|
||
|
"""
|
||
|
A scalar SPDX object property of a SPDX object
|
||
|
"""
|
||
|
|
||
|
def __init__(self, cls, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.cls = cls
|
||
|
|
||
|
def set_property(self, attrs, name):
|
||
|
def get_helper(obj):
|
||
|
if not name in obj._spdx:
|
||
|
obj._spdx[name] = self.cls()
|
||
|
return obj._spdx[name]
|
||
|
|
||
|
def set_helper(obj, value):
|
||
|
obj._spdx[name] = value
|
||
|
|
||
|
def del_helper(obj):
|
||
|
del obj._spdx[name]
|
||
|
|
||
|
attrs[name] = property(get_helper, set_helper)
|
||
|
|
||
|
def init(self, source):
|
||
|
return self.cls(**source)
|
||
|
|
||
|
|
||
|
class _ListProperty(_Property):
|
||
|
"""
|
||
|
A list of SPDX properties
|
||
|
"""
|
||
|
|
||
|
def __init__(self, prop, **kwargs):
|
||
|
super().__init__(**kwargs)
|
||
|
self.prop = prop
|
||
|
|
||
|
def set_property(self, attrs, name):
|
||
|
def get_helper(obj):
|
||
|
if not name in obj._spdx:
|
||
|
obj._spdx[name] = []
|
||
|
return obj._spdx[name]
|
||
|
|
||
|
def set_helper(obj, value):
|
||
|
obj._spdx[name] = list(value)
|
||
|
|
||
|
def del_helper(obj):
|
||
|
del obj._spdx[name]
|
||
|
|
||
|
attrs[name] = property(get_helper, set_helper, del_helper)
|
||
|
|
||
|
def init(self, source):
|
||
|
return [self.prop.init(o) for o in source]
|
||
|
|
||
|
|
||
|
class _StringList(_ListProperty):
|
||
|
"""
|
||
|
A list of strings as a property for an SPDX object
|
||
|
"""
|
||
|
|
||
|
def __init__(self, **kwargs):
|
||
|
super().__init__(_String(), **kwargs)
|
||
|
|
||
|
|
||
|
class _ObjectList(_ListProperty):
|
||
|
"""
|
||
|
A list of SPDX objects as a property for an SPDX object
|
||
|
"""
|
||
|
|
||
|
def __init__(self, cls, **kwargs):
|
||
|
super().__init__(_Object(cls), **kwargs)
|
||
|
|
||
|
|
||
|
class MetaSPDXObject(type):
|
||
|
"""
|
||
|
A metaclass that allows properties (anything derived from a _Property
|
||
|
class) to be defined for a SPDX object
|
||
|
"""
|
||
|
def __new__(mcls, name, bases, attrs):
|
||
|
attrs["_properties"] = {}
|
||
|
|
||
|
for key in attrs.keys():
|
||
|
if isinstance(attrs[key], _Property):
|
||
|
prop = attrs[key]
|
||
|
attrs["_properties"][key] = prop
|
||
|
prop.set_property(attrs, key)
|
||
|
|
||
|
return super().__new__(mcls, name, bases, attrs)
|
||
|
|
||
|
|
||
|
class SPDXObject(metaclass=MetaSPDXObject):
|
||
|
"""
|
||
|
The base SPDX object; all SPDX spec classes must derive from this class
|
||
|
"""
|
||
|
def __init__(self, **d):
|
||
|
self._spdx = {}
|
||
|
|
||
|
for name, prop in self._properties.items():
|
||
|
prop.setdefault(self._spdx, name)
|
||
|
if name in d:
|
||
|
self._spdx[name] = prop.init(d[name])
|
||
|
|
||
|
def serializer(self):
|
||
|
return self._spdx
|
||
|
|
||
|
def __setattr__(self, name, value):
|
||
|
if name in self._properties or name == "_spdx":
|
||
|
super().__setattr__(name, value)
|
||
|
return
|
||
|
raise KeyError("%r is not a valid SPDX property" % name)
|
||
|
|
||
|
#
|
||
|
# These are the SPDX objects implemented from the spec. The *only* properties
|
||
|
# that can be added to these objects are ones directly specified in the SPDX
|
||
|
# spec, however you may add helper functions to make operations easier.
|
||
|
#
|
||
|
# Defaults should *only* be specified if the SPDX spec says there is a certain
|
||
|
# required value for a field (e.g. dataLicense), or if the field is mandatory
|
||
|
# and has some sane "this field is unknown" (e.g. "NOASSERTION")
|
||
|
#
|
||
|
|
||
|
class SPDXAnnotation(SPDXObject):
|
||
|
annotationDate = _String()
|
||
|
annotationType = _String()
|
||
|
annotator = _String()
|
||
|
comment = _String()
|
||
|
|
||
|
class SPDXChecksum(SPDXObject):
|
||
|
algorithm = _String()
|
||
|
checksumValue = _String()
|
||
|
|
||
|
|
||
|
class SPDXRelationship(SPDXObject):
|
||
|
spdxElementId = _String()
|
||
|
relatedSpdxElement = _String()
|
||
|
relationshipType = _String()
|
||
|
comment = _String()
|
||
|
annotations = _ObjectList(SPDXAnnotation)
|
||
|
|
||
|
|
||
|
class SPDXExternalReference(SPDXObject):
|
||
|
referenceCategory = _String()
|
||
|
referenceType = _String()
|
||
|
referenceLocator = _String()
|
||
|
|
||
|
|
||
|
class SPDXPackageVerificationCode(SPDXObject):
|
||
|
packageVerificationCodeValue = _String()
|
||
|
packageVerificationCodeExcludedFiles = _StringList()
|
||
|
|
||
|
|
||
|
class SPDXPackage(SPDXObject):
|
||
|
name = _String()
|
||
|
SPDXID = _String()
|
||
|
versionInfo = _String()
|
||
|
downloadLocation = _String(default="NOASSERTION")
|
||
|
supplier = _String(default="NOASSERTION")
|
||
|
homepage = _String()
|
||
|
licenseConcluded = _String(default="NOASSERTION")
|
||
|
licenseDeclared = _String(default="NOASSERTION")
|
||
|
summary = _String()
|
||
|
description = _String()
|
||
|
sourceInfo = _String()
|
||
|
copyrightText = _String(default="NOASSERTION")
|
||
|
licenseInfoFromFiles = _StringList(default=["NOASSERTION"])
|
||
|
externalRefs = _ObjectList(SPDXExternalReference)
|
||
|
packageVerificationCode = _Object(SPDXPackageVerificationCode)
|
||
|
hasFiles = _StringList()
|
||
|
packageFileName = _String()
|
||
|
annotations = _ObjectList(SPDXAnnotation)
|
||
|
|
||
|
|
||
|
class SPDXFile(SPDXObject):
|
||
|
SPDXID = _String()
|
||
|
fileName = _String()
|
||
|
licenseConcluded = _String(default="NOASSERTION")
|
||
|
copyrightText = _String(default="NOASSERTION")
|
||
|
licenseInfoInFiles = _StringList(default=["NOASSERTION"])
|
||
|
checksums = _ObjectList(SPDXChecksum)
|
||
|
fileTypes = _StringList()
|
||
|
|
||
|
|
||
|
class SPDXCreationInfo(SPDXObject):
|
||
|
created = _String()
|
||
|
licenseListVersion = _String()
|
||
|
comment = _String()
|
||
|
creators = _StringList()
|
||
|
|
||
|
|
||
|
class SPDXExternalDocumentRef(SPDXObject):
|
||
|
externalDocumentId = _String()
|
||
|
spdxDocument = _String()
|
||
|
checksum = _Object(SPDXChecksum)
|
||
|
|
||
|
|
||
|
class SPDXExtractedLicensingInfo(SPDXObject):
|
||
|
name = _String()
|
||
|
comment = _String()
|
||
|
licenseId = _String()
|
||
|
extractedText = _String()
|
||
|
|
||
|
|
||
|
class SPDXDocument(SPDXObject):
|
||
|
spdxVersion = _String(default="SPDX-" + SPDX_VERSION)
|
||
|
dataLicense = _String(default="CC0-1.0")
|
||
|
SPDXID = _String(default="SPDXRef-DOCUMENT")
|
||
|
name = _String()
|
||
|
documentNamespace = _String()
|
||
|
creationInfo = _Object(SPDXCreationInfo)
|
||
|
packages = _ObjectList(SPDXPackage)
|
||
|
files = _ObjectList(SPDXFile)
|
||
|
relationships = _ObjectList(SPDXRelationship)
|
||
|
externalDocumentRefs = _ObjectList(SPDXExternalDocumentRef)
|
||
|
hasExtractedLicensingInfos = _ObjectList(SPDXExtractedLicensingInfo)
|
||
|
|
||
|
def __init__(self, **d):
|
||
|
super().__init__(**d)
|
||
|
|
||
|
def to_json(self, f, *, sort_keys=False, indent=None, separators=None):
|
||
|
class Encoder(json.JSONEncoder):
|
||
|
def default(self, o):
|
||
|
if isinstance(o, SPDXObject):
|
||
|
return o.serializer()
|
||
|
|
||
|
return super().default(o)
|
||
|
|
||
|
sha1 = hashlib.sha1()
|
||
|
for chunk in Encoder(
|
||
|
sort_keys=sort_keys,
|
||
|
indent=indent,
|
||
|
separators=separators,
|
||
|
).iterencode(self):
|
||
|
chunk = chunk.encode("utf-8")
|
||
|
f.write(chunk)
|
||
|
sha1.update(chunk)
|
||
|
|
||
|
return sha1.hexdigest()
|
||
|
|
||
|
@classmethod
|
||
|
def from_json(cls, f):
|
||
|
return cls(**json.load(f))
|
||
|
|
||
|
def add_relationship(self, _from, relationship, _to, *, comment=None, annotation=None):
|
||
|
if isinstance(_from, SPDXObject):
|
||
|
from_spdxid = _from.SPDXID
|
||
|
else:
|
||
|
from_spdxid = _from
|
||
|
|
||
|
if isinstance(_to, SPDXObject):
|
||
|
to_spdxid = _to.SPDXID
|
||
|
else:
|
||
|
to_spdxid = _to
|
||
|
|
||
|
r = SPDXRelationship(
|
||
|
spdxElementId=from_spdxid,
|
||
|
relatedSpdxElement=to_spdxid,
|
||
|
relationshipType=relationship,
|
||
|
)
|
||
|
|
||
|
if comment is not None:
|
||
|
r.comment = comment
|
||
|
|
||
|
if annotation is not None:
|
||
|
r.annotations.append(annotation)
|
||
|
|
||
|
self.relationships.append(r)
|
||
|
|
||
|
def find_by_spdxid(self, spdxid):
|
||
|
for o in itertools.chain(self.packages, self.files):
|
||
|
if o.SPDXID == spdxid:
|
||
|
return o
|
||
|
return None
|
||
|
|
||
|
def find_external_document_ref(self, namespace):
|
||
|
for r in self.externalDocumentRefs:
|
||
|
if r.spdxDocument == namespace:
|
||
|
return r
|
||
|
return None
|