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