#!/usr/bin/env python3 """Focused offline regressions for the provenance evidence collector.""" import io import json from pathlib import Path import shutil import sys import tarfile import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / 'tools')) import record_dependency_provenance as provenance class ProvenanceTest(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory(prefix='docview-provenance-test-') self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) def package(self, version='2:1.2.3-1', build_version=None, with_build=True): # The Arch cache collector deliberately uses Python's native zstd tar # support (3.14+). Only these five real .pkg.tar.zst fixture cases need # that capability; all package/notice boundaries still run on 3.11/3.12. try: with tarfile.open(fileobj=io.BytesIO(), mode='w:zst'): pass except (tarfile.CompressionError, ModuleNotFoundError): self.skipTest('Arch .pkg.tar.zst metadata tests require Python tarfile zstd support (3.14+)') package = ('pkgname = fixture\npkgbase = fixture\npkgver = ' + version + '\narch = x86_64\nlicense = MIT\nlicense = BSD-3-Clause\n' 'builddate = 12345\npackager = Private Person \n').encode() build = ('format = 2\npkgname = fixture\npkgbase = fixture\npkgver = ' + (build_version or version) + '\npkgarch = x86_64\npkgbuild_sha256sum = ' + 'a' * 64 + '\nbuildtool = devtools\nbuildtoolver = 1:1.2.3-1-any\n' 'builddate = 12345\nbuilddir = /home/private/work\n' 'packager = Private Person \ninstalled = private-tool-1.0\n').encode() path = self.root / f'fixture-{version}-x86_64.pkg.tar.zst' with tarfile.open(path, 'w:zst') as archive: entries = [('.PKGINFO', package)] if with_build: entries.insert(0, ('.BUILDINFO', build)) for name, data in entries: info = tarfile.TarInfo(name) info.size = len(data) archive.addfile(info, io.BytesIO(data)) return path, package, build def pdfium_fixture(self, supplemental=False): """Small physical package with the legacy 15 notices, optionally plus two.""" project, local, payload = [self.root / name for name in ('project', 'pdfium', 'payload')] (project / 'cmake').mkdir(parents=True) (local / 'lib').mkdir(parents=True) (payload / 'lib').mkdir(parents=True) lock = json.loads((ROOT / 'cmake/pdfium.lock.json').read_text()) (project / 'cmake/pdfium.lock.json').write_bytes(provenance.canonical(lock)) (local / 'VERSION').write_text(''.join(f'{key}={value}\n' for key, value in zip(('MAJOR', 'MINOR', 'BUILD', 'PATCH'), lock['version'].split('.')))) (local / 'args.gn').write_text(''.join(f'{key} = {json.dumps(value)}\n' for key, value in lock['buildOptions'].items())) library = b'fixture library whose bytes are bound to every metadata layer' for path in (local / 'lib/libpdfium.so', payload / 'lib/libpdfium.so'): path.write_bytes(library) component = {'version': lock['version'], 'path': 'lib/libpdfium.so', 'sha256': provenance.digest(library), 'size': len(library), 'source': {'upstreamCommit': lock['upstreamCommit'], 'archiveSha256': lock['linuxX64Sha256']}, 'licenseTexts': []} for index in range(15): suffix = 'LICENSE' if index == 0 else f'licenses/dependency-{index}.txt' data = f'Provider notice fixture {index}\n'.encode() target = payload / 'share/doc/docview/pdfium' / suffix target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(data) original = local / suffix original.parent.mkdir(parents=True, exist_ok=True) original.write_bytes(data) component['licenseTexts'].append({'path': target.relative_to(payload).as_posix(), 'size': len(data), 'sha256': provenance.digest(data)}) if supplemental: source = project / 'resources/licenses/pdfium-supplemental' shutil.copytree(ROOT / 'resources/licenses/pdfium-supplemental', source) metadata = json.loads((source / 'sources.json').read_text()) metadata['pdfiumLibrarySha256'] = provenance.digest(library) metadata_bytes = provenance.canonical(metadata) (source / 'sources.json').write_bytes(metadata_bytes) relative = 'share/doc/docview/pdfium/supplemental' shutil.copytree(source, payload / relative) rows = [] for row in metadata['files']: path = relative + '/' + row['name'] rows.append({**row, 'path': path}) component['licenseTexts'].append({'path': path, 'sha256': row['sha256'], 'size': row['size']}) component['supplementalNotices'] = {'metadataPath': relative + '/sources.json', 'metadataSha256': provenance.digest(metadata_bytes), 'scope': metadata['scope'], 'providerRecipeCommit': metadata['providerRecipeCommit'], 'pdfiumLibrarySha256': provenance.digest(library), 'files': rows} return payload, {'components': {'PDFium-independent-worker': component}}, local, project def test_legacy_pdfium_fifteen_notices_without_supplemental_are_accepted(self): result = provenance.pdfium_record(*self.pdfium_fixture()) self.assertEqual(len(result['licenseTexts']), 15) self.assertTrue(result['localLibraryMatchesPayload']) self.assertEqual(result['supplementalNotices']['status'], 'not-in-this-package') self.assertTrue(any('see separate source-correspondence' in row for row in result['remaining'])) self.assertFalse(any('not collected' in row for row in result['remaining'])) def test_pdfium_supplemental_binds_original_metadata_pin_library_and_two_notices(self): result = provenance.pdfium_record(*self.pdfium_fixture(supplemental=True)) supplements = result['supplementalNotices'] self.assertEqual(len(result['licenseTexts']), 17) self.assertEqual(supplements['status'], 'repository-original-and-package-matched') self.assertEqual(supplements['pdfiumLibrarySha256'], result['library']['sha256']) self.assertEqual(supplements['metadataSha256'], supplements['repositoryMetadata']['sha256']) self.assertEqual({row['component'] for row in supplements['files']}, {'libc++', 'libc++abi'}) self.assertTrue(all(len(row['sourceRevision']) == 40 for row in supplements['files'])) def test_supplemental_packaged_metadata_change_rejected(self): args = self.pdfium_fixture(supplemental=True) metadata = args[0] / 'share/doc/docview/pdfium/supplemental/sources.json' metadata.write_bytes(metadata.read_bytes() + b'\n') with self.assertRaisesRegex(ValueError, 'metadata differs from repository'): provenance.pdfium_record(*args) def test_supplemental_changed_original_notice_rejected(self): args = self.pdfium_fixture(supplemental=True) notice = args[3] / 'resources/licenses/pdfium-supplemental/libcxx-LICENSE.txt' notice.write_bytes(b'changed upstream notice') with self.assertRaisesRegex(ValueError, 'notice differs from original'): provenance.pdfium_record(*args) def test_supplemental_changed_packaged_notice_and_inventory_cannot_hide_mismatch(self): args = self.pdfium_fixture(supplemental=True) component = args[1]['components']['PDFium-independent-worker'] row = component['licenseTexts'][-1] data = b'changed notice with a matching outer inventory hash' (args[0] / row['path']).write_bytes(data) row.update(size=len(data), sha256=provenance.digest(data)) with self.assertRaisesRegex(ValueError, 'notice differs from original'): provenance.pdfium_record(*args) def test_supplemental_library_substitution_with_matching_outer_hash_rejected(self): args = self.pdfium_fixture(supplemental=True) library = b'a different library with copied notice files' for directory in (args[0], args[2]): (directory / 'lib/libpdfium.so').write_bytes(library) args[1]['components']['PDFium-independent-worker']['sha256'] = provenance.digest(library) with self.assertRaisesRegex(ValueError, 'source pin or library hash mismatch'): provenance.pdfium_record(*args) def test_supplemental_source_pin_substitution_rejected(self): args = self.pdfium_fixture(supplemental=True) for path in (args[0] / 'share/doc/docview/pdfium/supplemental/sources.json', args[3] / 'resources/licenses/pdfium-supplemental/sources.json'): metadata = json.loads(path.read_text()) metadata['pdfiumUpstreamCommit'] = '0' * 40 path.write_bytes(provenance.canonical(metadata)) with self.assertRaisesRegex(ValueError, 'source pin or library hash mismatch'): provenance.pdfium_record(*args) def test_supplemental_manifest_cannot_claim_a_different_component_revision(self): args = self.pdfium_fixture(supplemental=True) args[1]['components']['PDFium-independent-worker']['supplementalNotices']['files'][0]['sourceRevision'] = '0' * 40 with self.assertRaisesRegex(ValueError, 'manifest metadata mismatch'): provenance.pdfium_record(*args) def test_supplemental_payload_without_manifest_metadata_is_not_legacy(self): args = self.pdfium_fixture(supplemental=True) del args[1]['components']['PDFium-independent-worker']['supplementalNotices'] with self.assertRaisesRegex(ValueError, 'lacks manifest metadata'): provenance.pdfium_record(*args) def test_supplemental_metadata_cannot_omit_notice_from_license_inventory(self): args = self.pdfium_fixture(supplemental=True) args[1]['components']['PDFium-independent-worker']['licenseTexts'].pop() with self.assertRaisesRegex(ValueError, 'license inventory differs'): provenance.pdfium_record(*args) def test_supplemental_duplicate_license_row_rejected(self): args = self.pdfium_fixture(supplemental=True) rows = args[1]['components']['PDFium-independent-worker']['licenseTexts'] rows.append(dict(rows[-1])) with self.assertRaisesRegex(ValueError, 'Duplicate PDFium license'): provenance.pdfium_record(*args) def test_supplemental_unknown_file_rejected(self): args = self.pdfium_fixture(supplemental=True) (args[0] / 'share/doc/docview/pdfium/supplemental/extra.txt').write_bytes(b'unknown') with self.assertRaisesRegex(ValueError, 'payload contents'): provenance.pdfium_record(*args) def test_identity_matched_metadata_omits_personal_fields(self): path, package, build = self.package() result = provenance.cache_metadata(path, ('fixture', '2:1.2.3-1', 'x86_64', 'fixture')) encoded = provenance.canonical(result) for token in (b'/home/', b'private', b'packager', b'builddir', b'installed ='): self.assertNotIn(token, encoded) self.assertEqual(result['packageInfo']['rawSha256'], provenance.digest(package)) self.assertEqual(result['buildInfo']['rawSha256'], provenance.digest(build)) self.assertEqual(result['buildInfo']['selectedFields']['pkgbuild_sha256sum'], 'a' * 64) self.assertEqual(result['packageInfo']['selectedFields']['license'], ['BSD-3-Clause', 'MIT']) self.assertEqual(result['signatureVerification'], 'not-performed') self.assertEqual(result['installedFileToCachedPayloadComparison'], 'not-performed') self.assertIsNone(result['recipeCommit']) def test_epoch_is_part_of_cache_identity(self): self.package() item = {'version': '2:1.2.3-1', 'architecture': 'x86_64', 'source': {'packageBase': 'fixture'}} result = provenance.cache_record(self.root, 'fixture', item) self.assertEqual(result['status'], 'identity-matched-metadata') item['version'] = '1.2.3-1' self.assertEqual(provenance.cache_record(self.root, 'fixture', item), {'status': 'not-present-in-selected-cache'}) def test_build_version_mismatch_rejected(self): path, _, _ = self.package(build_version='2:1.2.4-1') with self.assertRaisesRegex(ValueError, 'identity'): provenance.cache_metadata(path, ('fixture', '2:1.2.3-1', 'x86_64', 'fixture')) def test_package_architecture_mismatch_rejected(self): path, _, _ = self.package() with self.assertRaisesRegex(ValueError, 'identity'): provenance.cache_metadata(path, ('fixture', '2:1.2.3-1', 'aarch64', 'fixture')) def test_missing_build_metadata_not_success(self): path, _, _ = self.package(with_build=False) with self.assertRaisesRegex(ValueError, 'lacks'): provenance.cache_metadata(path, ('fixture', '2:1.2.3-1', 'x86_64', 'fixture')) def test_duplicate_scalar_rejected(self): with self.assertRaisesRegex(ValueError, 'Duplicate'): provenance.parse_metadata(b'pkgver = 1\npkgver = 2\n', provenance.PKG_FIELDS) def test_sensitive_selected_value_rejected(self): with self.assertRaisesRegex(ValueError, 'path or address'): provenance.parse_metadata(b'pkgbase = /home/private\n', provenance.PKG_FIELDS) def test_metadata_bounds_and_nul(self): for data in (b'a' * (provenance.MAX_METADATA + 1), b'pkgname = bad\0name\n'): with self.assertRaises(ValueError): provenance.parse_metadata(data, provenance.PKG_FIELDS) def test_license_hash_mismatch_rejected(self): (self.root / 'LICENSE').write_bytes(b'changed license') with self.assertRaisesRegex(ValueError, 'disagrees'): provenance.license_evidence(self.root, {'path': 'LICENSE', 'sha256': '0' * 64, 'size': 15}, bundled=True) def test_license_path_traversal_rejected(self): with self.assertRaisesRegex(ValueError, 'Unsafe'): provenance.license_evidence(self.root, {'path': '../LICENSE', 'sha256': '0' * 64, 'size': 1}, bundled=True) def test_canonical_bytes_ignore_dictionary_insertion_order(self): first = {'z': 1, 'a': {'b': 3, 'a': 2}} second = {'a': {'a': 2, 'b': 3}, 'z': 1} self.assertEqual(provenance.canonical(first), provenance.canonical(second)) if __name__ == '__main__': unittest.main(verbosity=2)