test_redistribution.py (4505B)
1 """Fast source/notice ownership, integrity and archive-safety controls.""" 2 import hashlib 3 import io 4 import json 5 from pathlib import Path 6 import tarfile 7 import tempfile 8 import unittest 9 10 from redistribution import license_name, notices, owner 11 from verify_redistribution import relative, verify, verify_archive 12 13 14 class RedistributionTests(unittest.TestCase): 15 def test_notice_names(self): 16 for name in ('LICENSES', 'COPYING.RUNTIME', 'COPYING3', 'COPYING.LESSERv3', 'LICENSE.LGPL2.1'): 17 self.assertTrue(license_name(name), name) 18 for name in ('License.hs', 'licenses.json', 'license-check.sh', 'other'): 19 self.assertFalse(license_name(name), name) 20 21 def test_owners(self): 22 with tempfile.TemporaryDirectory() as directory: 23 root = Path(directory) 24 (root/'input.a').write_bytes(b'archive') 25 row = dict(name='library', roots=[str(root)]) 26 self.assertEqual(owner(root/'input.a', [row]), row) 27 for rows in ([], [row, row]): 28 with self.assertRaises(ValueError): 29 owner(root/'input.a', rows) 30 31 def test_embedded_notice(self): 32 with tempfile.TemporaryDirectory() as directory: 33 archive = Path(directory)/'source.tar.gz' 34 block = b'/* Copyright Example. Redistribution permitted. */\n' 35 with tarfile.open(archive, 'w:gz') as out: 36 for name, body in [('pkg/LICENSES', b'Complete license'), 37 ('pkg/cbits/hash.c', block+b'int value;')]: 38 item = tarfile.TarInfo(name) 39 item.size = len(body) 40 out.addfile(item, io.BytesIO(body)) 41 self.assertEqual(dict(notices(archive, 'example')), { 42 'pkg/LICENSES': b'Complete license', 'pkg/cbits/hash.c': block}) 43 44 def test_archive_pairing(self): 45 with tempfile.TemporaryDirectory() as directory: 46 archive = Path(directory)/'release.tar.gz' 47 with tarfile.open(archive, 'w:gz') as out: 48 item = tarfile.TarInfo('release/file') 49 item.size = 4 50 out.addfile(item, io.BytesIO(b'data')) 51 expected = {'file': hashlib.sha256(b'data').hexdigest()} 52 verify_archive(archive, 'release', expected, {}) 53 for prefix, hashes in [('wrong-prefix', expected), ('release', {}), 54 ('release', {'file': 'changed'})]: 55 with self.assertRaises(ValueError): 56 verify_archive(archive, prefix, hashes, {}) 57 58 def test_verifier_controls(self): 59 with tempfile.TemporaryDirectory() as directory: 60 root = Path(directory) 61 (root/'sources/example').mkdir(parents=True) 62 (root/'notices').mkdir() 63 (root/'sources/example/upstream').write_bytes(b'original source') 64 (root/'notices/LICENSE').write_bytes(b'original license') 65 checksum = hashlib.sha256(b'original license').hexdigest() 66 (root/'components.json').write_text(json.dumps(dict(components=[dict( 67 name='example', upstream_file='upstream', 68 notices=[dict(file='notices/LICENSE', sha256=checksum)])]))) 69 (root/'source-link').symlink_to('sources/example/upstream') 70 (root/'SYMLINKS.json').write_text(json.dumps({'source-link': 'sources/example/upstream'})) 71 hashes = {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() 72 for p in root.rglob('*') if p.is_file() and not p.is_symlink() and p.name != 'SYMLINKS.json'} 73 (root/'SHA256.json').write_text(json.dumps(hashes)) 74 self.assertEqual(verify(root)['components'], 1) 75 (root/'notices/LICENSE').write_bytes(b'altered license') 76 with self.assertRaises(ValueError): 77 verify(root) 78 (root/'notices/LICENSE').write_bytes(b'original license') 79 (root/'extra').write_text('unexpected') 80 with self.assertRaises(ValueError): 81 verify(root) 82 (root/'extra').unlink() 83 (root/'source-link').unlink() 84 (root/'source-link').symlink_to('elsewhere') 85 with self.assertRaises(ValueError): 86 verify(root) 87 for path in ('../outside', '/outside'): 88 with self.assertRaises(ValueError): 89 relative(root, path) 90 91 92 if __name__ == '__main__': 93 unittest.main()