import hashlib import importlib.util import io import json import contextlib import pathlib import tarfile import tempfile import unittest spec = importlib.util.spec_from_file_location('bootstrap', pathlib.Path(__file__).parents[1] / 'tools/bootstrap.py') bootstrap = importlib.util.module_from_spec(spec) spec.loader.exec_module(bootstrap) class BootstrapTests(unittest.TestCase): def test_external_checksums_match_exact_name_and_reject_tampering(self): with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) archive = root / 'framely-1-linux-arm64.tar.gz' sums = root / 'SHA256SUMS' archive.write_bytes(b'payload') entry = hashlib.sha256(b'payload').hexdigest() + ' ' + archive.name + '\n' sums.write_text(entry) bootstrap.verify_archive(archive, sums) archive.write_bytes(b'changed') with self.assertRaises(ValueError): bootstrap.verify_archive(archive, sums) sums.write_text(entry * 2) with self.assertRaises(ValueError): bootstrap.verify_archive(archive, sums) sums.write_text(entry.replace(archive.name, './' + archive.name)) with self.assertRaises(ValueError): bootstrap.verify_archive(archive, sums) def archive(self, root, entries): archive = root / 'release.tar.gz' with tarfile.open(archive, 'w:gz') as out: for name, contents in entries: member = tarfile.TarInfo(name) if contents is None: member.type = tarfile.SYMTYPE member.linkname = '/etc/shadow' out.addfile(member) else: member.size = len(contents) out.addfile(member, io.BytesIO(contents)) return archive def test_safe_extraction_checks_structure_before_writing(self): entries = [('framely-1/' + name, b'1' if name == 'VERSION' else b'test') for name in ['VERSION', 'SHA256SUMS', 'install.sh', 'uninstall.sh', 'bin/framely']] with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) base = bootstrap.extract_archive(self.archive(root, entries), root / 'out') self.assertEqual(base.name, 'framely-1') for extra in [('framely-1/../../escape', b'bad'), ('framely-1/link', None), ('framely-1/VERSION', b'1'), ('framely-2/file', b'bad')]: with self.subTest(extra=extra), tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) with self.assertRaises(ValueError): bootstrap.extract_archive(self.archive(root, entries + [extra]), root / 'out') self.assertFalse((root / 'out').exists()) def test_redirect_cannot_downgrade_https(self): with self.assertRaises(ValueError): bootstrap.HTTPSRedirect().redirect_request(None, None, 302, '', {}, 'http://example.org/package') def test_extraction_progress_reports_real_bytes_and_never_completes_invalid_release(self): entries = [('framely-1/' + name, b'1' if name == 'VERSION' else b'test') for name in ['VERSION', 'SHA256SUMS', 'install.sh', 'uninstall.sh', 'bin/framely']] with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) reports = [] bootstrap.extract_archive(self.archive(root, entries), root / 'out', lambda *value: reports.append(value)) total = sum(len(contents) for _, contents in entries) self.assertEqual(reports[0], ('extract', 0, total)) self.assertEqual(reports[-1], ('extract', total, total)) with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) reports = [] with self.assertRaises(ValueError): bootstrap.extract_archive(self.archive(root, [('framely-1/VERSION', b'wrong')]), root / 'out', lambda *value: reports.append(value)) self.assertFalse(any(done == total and total > 0 for _, done, total in reports)) output = io.StringIO() with contextlib.redirect_stdout(output): bootstrap.emit_progress('configure') payload = json.loads(output.getvalue().removeprefix('FRAMELY_PROGRESS ')) self.assertEqual(payload, {'step': 'configure'}) if __name__ == '__main__': unittest.main()