| # |
| # Copyright OpenEmbedded Contributors |
| # |
| # 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): |
| ALLOWED_CHECKSUMS = [ |
| "SHA1", |
| "SHA224", |
| "SHA256", |
| "SHA384", |
| "SHA512", |
| "MD2", |
| "MD4", |
| "MD5", |
| "MD6", |
| ] |
| |
| 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) |
| checksums = _ObjectList(SPDXChecksum) |
| |
| |
| 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 |