1# Copyright (c) 2020, 2021 The Linux Foundation
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import re
6from datetime import datetime
7
8from west import log
9
10from zspdx.util import getHashes
11from zspdx.version import SPDX_VERSION_2_3
12
13CPE23TYPE_REGEX = (
14    r'^cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^'
15    r"`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*"
16    r'|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){4}$'
17)
18PURL_REGEX = r"^pkg:.+(\/.+)?\/.+(@.+)?(\?.+)?(#.+)?$"
19
20def _normalize_spdx_name(name):
21    # Replace "_" by "-" since it's not allowed in spdx ID
22    return name.replace("_", "-")
23
24# Output tag-value SPDX 2.3 content for the given Relationship object.
25# Arguments:
26#   1) f: file handle for SPDX document
27#   2) rln: Relationship object being described
28def writeRelationshipSPDX(f, rln):
29    f.write(
30        f"Relationship: {_normalize_spdx_name(rln.refA)} {rln.rlnType} "
31        f"{_normalize_spdx_name(rln.refB)}\n"
32    )
33
34# Output tag-value SPDX 2.3 content for the given File object.
35# Arguments:
36#   1) f: file handle for SPDX document
37#   2) bf: File object being described
38def writeFileSPDX(f, bf):
39    spdx_normalize_spdx_id = _normalize_spdx_name(bf.spdxID)
40
41    f.write(f"""FileName: ./{bf.relpath}
42SPDXID: {spdx_normalize_spdx_id}
43FileChecksum: SHA1: {bf.sha1}
44""")
45    if bf.sha256 != "":
46        f.write(f"FileChecksum: SHA256: {bf.sha256}\n")
47    if bf.md5 != "":
48        f.write(f"FileChecksum: MD5: {bf.md5}\n")
49    f.write(f"LicenseConcluded: {bf.concludedLicense}\n")
50    if len(bf.licenseInfoInFile) == 0:
51        f.write("LicenseInfoInFile: NONE\n")
52    else:
53        for licInfoInFile in bf.licenseInfoInFile:
54            f.write(f"LicenseInfoInFile: {licInfoInFile}\n")
55    f.write(f"FileCopyrightText: {bf.copyrightText}\n\n")
56
57    # write file relationships
58    if len(bf.rlns) > 0:
59        for rln in bf.rlns:
60            writeRelationshipSPDX(f, rln)
61        f.write("\n")
62
63def generateDowloadUrl(url, revision):
64    # Only git is supported
65    # walker.py only parse revision if it's from git repositiory
66    if len(revision) == 0:
67        return url
68
69    return f'git+{url}@{revision}'
70
71# Output tag-value SPDX content for the given Package object.
72# Arguments:
73#   1) f: file handle for SPDX document
74#   2) pkg: Package object being described
75#   3) spdx_version: SPDX specification version
76def writePackageSPDX(f, pkg, spdx_version=SPDX_VERSION_2_3):
77    spdx_normalized_name = _normalize_spdx_name(pkg.cfg.name)
78    spdx_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID)
79
80    f.write(f"""##### Package: {spdx_normalized_name}
81
82PackageName: {spdx_normalized_name}
83SPDXID: {spdx_normalize_spdx_id}
84PackageLicenseConcluded: {pkg.concludedLicense}
85""")
86    f.write(f"""PackageLicenseDeclared: {pkg.cfg.declaredLicense}
87PackageCopyrightText: {pkg.cfg.copyrightText}
88""")
89
90    # PrimaryPackagePurpose is only available in SPDX 2.3 and later
91    if spdx_version >= SPDX_VERSION_2_3 and pkg.cfg.primaryPurpose != "":
92        f.write(f"PrimaryPackagePurpose: {pkg.cfg.primaryPurpose}\n")
93
94    if len(pkg.cfg.url) > 0:
95        downloadUrl = generateDowloadUrl(pkg.cfg.url, pkg.cfg.revision)
96        f.write(f"PackageDownloadLocation: {downloadUrl}\n")
97    else:
98        f.write("PackageDownloadLocation: NOASSERTION\n")
99
100    if len(pkg.cfg.version) > 0:
101        f.write(f"PackageVersion: {pkg.cfg.version}\n")
102    elif len(pkg.cfg.revision) > 0:
103        f.write(f"PackageVersion: {pkg.cfg.revision}\n")
104
105    for ref in pkg.cfg.externalReferences:
106        if re.fullmatch(CPE23TYPE_REGEX, ref):
107            f.write(f"ExternalRef: SECURITY cpe23Type {ref}\n")
108        elif re.fullmatch(PURL_REGEX, ref):
109            f.write(f"ExternalRef: PACKAGE_MANAGER purl {ref}\n")
110        else:
111            log.wrn(f"Unknown external reference ({ref})")
112
113    # flag whether files analyzed / any files present
114    if len(pkg.files) > 0:
115        if len(pkg.licenseInfoFromFiles) > 0:
116            for licFromFiles in pkg.licenseInfoFromFiles:
117                f.write(f"PackageLicenseInfoFromFiles: {licFromFiles}\n")
118        else:
119            f.write("PackageLicenseInfoFromFiles: NOASSERTION\n")
120        f.write(f"FilesAnalyzed: true\nPackageVerificationCode: {pkg.verificationCode}\n\n")
121    else:
122        f.write("FilesAnalyzed: false\nPackageComment: Utility target; no files\n\n")
123
124    # write package relationships
125    if len(pkg.rlns) > 0:
126        for rln in pkg.rlns:
127            writeRelationshipSPDX(f, rln)
128        f.write("\n")
129
130    # write package files, if any
131    if len(pkg.files) > 0:
132        bfs = list(pkg.files.values())
133        bfs.sort(key = lambda x: x.relpath)
134        for bf in bfs:
135            writeFileSPDX(f, bf)
136
137# Output tag-value SPDX 2.3 content for a custom license.
138# Arguments:
139#   1) f: file handle for SPDX document
140#   2) lic: custom license ID being described
141def writeOtherLicenseSPDX(f, lic):
142    f.write(f"""LicenseID: {lic}
143ExtractedText: {lic}
144LicenseName: {lic}
145LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag.
146""")
147
148# Output tag-value SPDX content for the given Document object.
149# Arguments:
150#   1) f: file handle for SPDX document
151#   2) doc: Document object being described
152#   3) spdx_version: SPDX specification version
153def writeDocumentSPDX(f, doc, spdx_version=SPDX_VERSION_2_3):
154    spdx_normalized_name = _normalize_spdx_name(doc.cfg.name)
155
156    f.write(f"""SPDXVersion: SPDX-{spdx_version}
157DataLicense: CC0-1.0
158SPDXID: SPDXRef-DOCUMENT
159DocumentName: {spdx_normalized_name}
160DocumentNamespace: {doc.cfg.namespace}
161Creator: Tool: Zephyr SPDX builder
162Created: {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}
163
164""")
165
166    # write any external document references
167    if len(doc.externalDocuments) > 0:
168        extDocs = list(doc.externalDocuments)
169        extDocs.sort(key = lambda x: x.cfg.docRefID)
170        for extDoc in extDocs:
171            f.write(
172                f"ExternalDocumentRef: {extDoc.cfg.docRefID} {extDoc.cfg.namespace} "
173                f"SHA1: {extDoc.myDocSHA1}\n"
174            )
175        f.write("\n")
176
177    # write relationships owned by this Document (not by its Packages, etc.), if any
178    if len(doc.relationships) > 0:
179        for rln in doc.relationships:
180            writeRelationshipSPDX(f, rln)
181        f.write("\n")
182
183    # write packages
184    for pkg in doc.pkgs.values():
185        writePackageSPDX(f, pkg, spdx_version)
186
187    # write other license info, if any
188    if len(doc.customLicenseIDs) > 0:
189        for lic in sorted(list(doc.customLicenseIDs)):
190            writeOtherLicenseSPDX(f, lic)
191
192# Open SPDX document file for writing, write the document, and calculate
193# its hash for other referring documents to use.
194# Arguments:
195#   1) spdxPath: path to write SPDX document
196#   2) doc: SPDX Document object to write
197#   3) spdx_version: SPDX specification version
198def writeSPDX(spdxPath, doc, spdx_version=SPDX_VERSION_2_3):
199    # create and write document to disk
200    try:
201        log.inf(f"Writing SPDX {spdx_version} document {doc.cfg.name} to {spdxPath}")
202        with open(spdxPath, "w") as f:
203            writeDocumentSPDX(f, doc, spdx_version)
204    except OSError as e:
205        log.err(f"Error: Unable to write to {spdxPath}: {str(e)}")
206        return False
207
208    # calculate hash of the document we just wrote
209    hashes = getHashes(spdxPath)
210    if not hashes:
211        log.err("Error: created document but unable to calculate hash values")
212        return False
213    doc.myDocSHA1 = hashes[0]
214
215    return True
216