#!/usr/bin/env python3 """Recover two explicitly declared Patent files omitted by the fixed Qt SDK SBOM.""" import argparse import hashlib import json from pathlib import Path import re ROOT = Path(__file__).resolve().parents[2] BASE = ROOT / 'tests/results/source-archives/qt-sdk-notices-final/report.json' BASE_SHA = 'cea0a2f80ad50b5519fbad94c1dd2eed55f5ffa34e83d1360ce8da6902a845a0' INVENTORY_SHA = '43dc4aa3cf0d970fc97a9db2555f0025429e9bd31823cb794fd5526496d1a9e0' SOURCE = ROOT / '.deps/source-archives/qt-6.11.2-regular-files' PREFIX = 'qtwebengine/src/3rdparty/chromium/' PATENTS = { 'libwebm': ('source/LICENSE.TXT', 'source/PATENTS.TXT'), 'libwebp': ('src/COPYING', 'src/PATENTS'), } ID_ONLY = { 'mini-chromium': ('third_party/crashpad/crashpad/third_party/mini_chromium/README.crashpad', 'Shipped in Chromium'), 'libdrm': ('third_party/libdrm/README.chromium', 'Shipped'), } def sha(path): with path.open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', type=Path, required=True) args = parser.parse_args() output = args.output.resolve() if not output.is_relative_to(ROOT): parser.error('Output must be inside the workspace') assert sha(BASE) == BASE_SHA base = json.loads(BASE.read_text()) inventory = ROOT / 'tests/results/source-archives/qt-inspection/files.jsonl' assert sha(inventory) == INVENTORY_SHA names = {PREFIX + 'tools/licenses/sbom.py'} for name, files in PATENTS.items(): names.update(PREFIX + 'third_party/' + name + '/' + tail for tail in (*files, 'README.chromium')) names.update(PREFIX + row[0] for row in ID_ONLY.values()) sources = {} for line in inventory.open(): row = json.loads(line) if row['path'] in names: assert row['path'] not in sources source = SOURCE / row['path'] assert source.is_file() and not source.is_symlink() assert source.stat().st_size == row['bytes'] and sha(source) == row['sha256'] sources[row['path']] = row assert sources.keys() == names assert sources[PREFIX + 'tools/licenses/sbom.py']['sha256'] == base['generatorSources'][PREFIX + 'tools/licenses/sbom.py'] packages = {p['SPDXID']: p for p in base['packages']} output.mkdir(parents=True, exist_ok=False) copied, notices, id_only = [], [], [] def copy(name, kind): source = sources[name] target = 'sources/' + name.removeprefix(PREFIX) path = output / target path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes((SOURCE / name).read_bytes()) assert sha(path) == source['sha256'] copied.append(dict(file=target, source=name, sha256=source['sha256'], bytes=source['bytes'], kind=kind)) return target def fields(name): return dict(re.findall(r'^([^:\n]+): (.*)$', (SOURCE / name).read_text(), re.M)) for name, (license_file, patent_file) in PATENTS.items(): directory = PREFIX + 'third_party/' + name + '/' metadata = directory + 'README.chromium' data = fields(metadata) assert data['License'] == 'BSD-3-Clause, Patent' and data['Shipped'] == 'yes' assert data['License File'] == license_file + ', ' + patent_file package_id = 'SPDXRef-Package-QtWebEngine-Chromium-WebEngine-' + name package = packages[package_id] assert package['comment'].splitlines()[0] == 'Location within source: third_party/' + name embedded = next(n for n in base['notices'] if package_id in n['packageIds']) assert embedded['source'] == directory + license_file assert embedded['sha256'] == sources[directory + license_file]['sha256'] assert not any(n['source'] == directory + patent_file for n in base['notices']) metadata_file = copy(metadata, 'upstream-package-metadata') notice_file = copy(directory + patent_file, 'declared-patent-notice') notices.append(dict(packageId=package_id, revision=data['Revision'], licenseDeclared=data['License'], metadataFile=metadata_file, noticeFile=notice_file, existingLicenseSha256=embedded['sha256'], reason='Second declared file omitted by SDK generator Patent branch')) for name, (relative, shipped_field) in ID_ONLY.items(): metadata = PREFIX + relative data = fields(metadata) assert data[shipped_field] == 'no' package_id = 'SPDXRef-Package-QtWebEngine-Chromium-WebEngine-' + name package = next(p for p in base['packagesWithoutEmbeddedNoticeText'] if p['SPDXID'] == package_id) assert data['License'] == package['licenseConcluded'] id_only.append(dict(packageId=package_id, metadataFile=copy(metadata, 'upstream-package-metadata'), declaredShippedField=shipped_field, declaredShippedValue='no', licenseId=package['licenseConcluded'], noticeAdded=False, linkedInSdk='unverified; upstream metadata is not a build-graph proof')) report = dict(schemaVersion=1, success=True, collectorSha256=sha(Path(__file__)), baseNoticeReportSha256=BASE_SHA, sourceInventorySha256=INVENTORY_SHA, qtSourceArchiveSha256=base['qtSourceArchiveSha256'], sdkArchiveSha256=base['sdkArchive']['sha256'], runtimeComparisons=base['testedRuntimeComparisons'], files=sorted(copied, key=lambda x:x['file']), sourceChecks=sorted(sources.values(), key=lambda x:x['path']), notices=notices, idOnlyPackages=id_only, sourceSdkExternalPackageReferencesValid=base['externalPackageReferencesValid'], completeChromiumNotices=False, completeCorrespondingSources=False, publicReleaseApproved=False, scope='Two upstream-declared Patent files recovered from the fixed Qt 6.11.2 source archive. ' 'Four upstream metadata files retained. Original SBOM is unchanged. ' 'Does not resolve the SDK build graph, generator skips or external package-ID inconsistency.') (output / 'report.json').write_text(json.dumps(report, indent=2, sort_keys=True) + '\n') print(json.dumps({'success': True, 'additionalNotices': len(notices), 'metadataFiles': len(copied)-len(notices)})) if __name__ == '__main__': main()