1# 2# SPDX-License-Identifier: GPL-2.0-only 3# 4 5# 6# This library is intended to capture the JSON SPDX specification in a type 7# safe manner. It is not intended to encode any particular OE specific 8# behaviors, see the sbom.py for that. 9# 10# The documented SPDX spec document doesn't cover the JSON syntax for 11# particular configuration, which can make it hard to determine what the JSON 12# syntax should be. I've found it is actually much simpler to read the official 13# SPDX JSON schema which can be found here: https://github.com/spdx/spdx-spec 14# in schemas/spdx-schema.json 15# 16 17import hashlib 18import itertools 19import json 20 21SPDX_VERSION = "2.2" 22 23 24# 25# The following are the support classes that are used to implement SPDX object 26# 27 28class _Property(object): 29 """ 30 A generic SPDX object property. The different types will derive from this 31 class 32 """ 33 34 def __init__(self, *, default=None): 35 self.default = default 36 37 def setdefault(self, dest, name): 38 if self.default is not None: 39 dest.setdefault(name, self.default) 40 41 42class _String(_Property): 43 """ 44 A scalar string property for an SPDX object 45 """ 46 47 def __init__(self, **kwargs): 48 super().__init__(**kwargs) 49 50 def set_property(self, attrs, name): 51 def get_helper(obj): 52 return obj._spdx[name] 53 54 def set_helper(obj, value): 55 obj._spdx[name] = value 56 57 def del_helper(obj): 58 del obj._spdx[name] 59 60 attrs[name] = property(get_helper, set_helper, del_helper) 61 62 def init(self, source): 63 return source 64 65 66class _Object(_Property): 67 """ 68 A scalar SPDX object property of a SPDX object 69 """ 70 71 def __init__(self, cls, **kwargs): 72 super().__init__(**kwargs) 73 self.cls = cls 74 75 def set_property(self, attrs, name): 76 def get_helper(obj): 77 if not name in obj._spdx: 78 obj._spdx[name] = self.cls() 79 return obj._spdx[name] 80 81 def set_helper(obj, value): 82 obj._spdx[name] = value 83 84 def del_helper(obj): 85 del obj._spdx[name] 86 87 attrs[name] = property(get_helper, set_helper) 88 89 def init(self, source): 90 return self.cls(**source) 91 92 93class _ListProperty(_Property): 94 """ 95 A list of SPDX properties 96 """ 97 98 def __init__(self, prop, **kwargs): 99 super().__init__(**kwargs) 100 self.prop = prop 101 102 def set_property(self, attrs, name): 103 def get_helper(obj): 104 if not name in obj._spdx: 105 obj._spdx[name] = [] 106 return obj._spdx[name] 107 108 def set_helper(obj, value): 109 obj._spdx[name] = list(value) 110 111 def del_helper(obj): 112 del obj._spdx[name] 113 114 attrs[name] = property(get_helper, set_helper, del_helper) 115 116 def init(self, source): 117 return [self.prop.init(o) for o in source] 118 119 120class _StringList(_ListProperty): 121 """ 122 A list of strings as a property for an SPDX object 123 """ 124 125 def __init__(self, **kwargs): 126 super().__init__(_String(), **kwargs) 127 128 129class _ObjectList(_ListProperty): 130 """ 131 A list of SPDX objects as a property for an SPDX object 132 """ 133 134 def __init__(self, cls, **kwargs): 135 super().__init__(_Object(cls), **kwargs) 136 137 138class MetaSPDXObject(type): 139 """ 140 A metaclass that allows properties (anything derived from a _Property 141 class) to be defined for a SPDX object 142 """ 143 def __new__(mcls, name, bases, attrs): 144 attrs["_properties"] = {} 145 146 for key in attrs.keys(): 147 if isinstance(attrs[key], _Property): 148 prop = attrs[key] 149 attrs["_properties"][key] = prop 150 prop.set_property(attrs, key) 151 152 return super().__new__(mcls, name, bases, attrs) 153 154 155class SPDXObject(metaclass=MetaSPDXObject): 156 """ 157 The base SPDX object; all SPDX spec classes must derive from this class 158 """ 159 def __init__(self, **d): 160 self._spdx = {} 161 162 for name, prop in self._properties.items(): 163 prop.setdefault(self._spdx, name) 164 if name in d: 165 self._spdx[name] = prop.init(d[name]) 166 167 def serializer(self): 168 return self._spdx 169 170 def __setattr__(self, name, value): 171 if name in self._properties or name == "_spdx": 172 super().__setattr__(name, value) 173 return 174 raise KeyError("%r is not a valid SPDX property" % name) 175 176# 177# These are the SPDX objects implemented from the spec. The *only* properties 178# that can be added to these objects are ones directly specified in the SPDX 179# spec, however you may add helper functions to make operations easier. 180# 181# Defaults should *only* be specified if the SPDX spec says there is a certain 182# required value for a field (e.g. dataLicense), or if the field is mandatory 183# and has some sane "this field is unknown" (e.g. "NOASSERTION") 184# 185 186class SPDXAnnotation(SPDXObject): 187 annotationDate = _String() 188 annotationType = _String() 189 annotator = _String() 190 comment = _String() 191 192class SPDXChecksum(SPDXObject): 193 algorithm = _String() 194 checksumValue = _String() 195 196 197class SPDXRelationship(SPDXObject): 198 spdxElementId = _String() 199 relatedSpdxElement = _String() 200 relationshipType = _String() 201 comment = _String() 202 annotations = _ObjectList(SPDXAnnotation) 203 204 205class SPDXExternalReference(SPDXObject): 206 referenceCategory = _String() 207 referenceType = _String() 208 referenceLocator = _String() 209 210 211class SPDXPackageVerificationCode(SPDXObject): 212 packageVerificationCodeValue = _String() 213 packageVerificationCodeExcludedFiles = _StringList() 214 215 216class SPDXPackage(SPDXObject): 217 name = _String() 218 SPDXID = _String() 219 versionInfo = _String() 220 downloadLocation = _String(default="NOASSERTION") 221 supplier = _String(default="NOASSERTION") 222 homepage = _String() 223 licenseConcluded = _String(default="NOASSERTION") 224 licenseDeclared = _String(default="NOASSERTION") 225 summary = _String() 226 description = _String() 227 sourceInfo = _String() 228 copyrightText = _String(default="NOASSERTION") 229 licenseInfoFromFiles = _StringList(default=["NOASSERTION"]) 230 externalRefs = _ObjectList(SPDXExternalReference) 231 packageVerificationCode = _Object(SPDXPackageVerificationCode) 232 hasFiles = _StringList() 233 packageFileName = _String() 234 annotations = _ObjectList(SPDXAnnotation) 235 236 237class SPDXFile(SPDXObject): 238 SPDXID = _String() 239 fileName = _String() 240 licenseConcluded = _String(default="NOASSERTION") 241 copyrightText = _String(default="NOASSERTION") 242 licenseInfoInFiles = _StringList(default=["NOASSERTION"]) 243 checksums = _ObjectList(SPDXChecksum) 244 fileTypes = _StringList() 245 246 247class SPDXCreationInfo(SPDXObject): 248 created = _String() 249 licenseListVersion = _String() 250 comment = _String() 251 creators = _StringList() 252 253 254class SPDXExternalDocumentRef(SPDXObject): 255 externalDocumentId = _String() 256 spdxDocument = _String() 257 checksum = _Object(SPDXChecksum) 258 259 260class SPDXExtractedLicensingInfo(SPDXObject): 261 name = _String() 262 comment = _String() 263 licenseId = _String() 264 extractedText = _String() 265 266 267class SPDXDocument(SPDXObject): 268 spdxVersion = _String(default="SPDX-" + SPDX_VERSION) 269 dataLicense = _String(default="CC0-1.0") 270 SPDXID = _String(default="SPDXRef-DOCUMENT") 271 name = _String() 272 documentNamespace = _String() 273 creationInfo = _Object(SPDXCreationInfo) 274 packages = _ObjectList(SPDXPackage) 275 files = _ObjectList(SPDXFile) 276 relationships = _ObjectList(SPDXRelationship) 277 externalDocumentRefs = _ObjectList(SPDXExternalDocumentRef) 278 hasExtractedLicensingInfos = _ObjectList(SPDXExtractedLicensingInfo) 279 280 def __init__(self, **d): 281 super().__init__(**d) 282 283 def to_json(self, f, *, sort_keys=False, indent=None, separators=None): 284 class Encoder(json.JSONEncoder): 285 def default(self, o): 286 if isinstance(o, SPDXObject): 287 return o.serializer() 288 289 return super().default(o) 290 291 sha1 = hashlib.sha1() 292 for chunk in Encoder( 293 sort_keys=sort_keys, 294 indent=indent, 295 separators=separators, 296 ).iterencode(self): 297 chunk = chunk.encode("utf-8") 298 f.write(chunk) 299 sha1.update(chunk) 300 301 return sha1.hexdigest() 302 303 @classmethod 304 def from_json(cls, f): 305 return cls(**json.load(f)) 306 307 def add_relationship(self, _from, relationship, _to, *, comment=None, annotation=None): 308 if isinstance(_from, SPDXObject): 309 from_spdxid = _from.SPDXID 310 else: 311 from_spdxid = _from 312 313 if isinstance(_to, SPDXObject): 314 to_spdxid = _to.SPDXID 315 else: 316 to_spdxid = _to 317 318 r = SPDXRelationship( 319 spdxElementId=from_spdxid, 320 relatedSpdxElement=to_spdxid, 321 relationshipType=relationship, 322 ) 323 324 if comment is not None: 325 r.comment = comment 326 327 if annotation is not None: 328 r.annotations.append(annotation) 329 330 self.relationships.append(r) 331 332 def find_by_spdxid(self, spdxid): 333 for o in itertools.chain(self.packages, self.files): 334 if o.SPDXID == spdxid: 335 return o 336 return None 337 338 def find_external_document_ref(self, namespace): 339 for r in self.externalDocumentRefs: 340 if r.spdxDocument == namespace: 341 return r 342 return None 343