118 lines
6.4 KiB
Python
118 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Boundaries of Ubuntu staging and preservation of launcher arguments."""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'tools'))
|
|
import package_ubuntu as package
|
|
|
|
|
|
class UbuntuPackageTests(unittest.TestCase):
|
|
def supplement_inputs(self, root):
|
|
source = ROOT / 'tests/results/qt-notice-supplement/collection'
|
|
directory = root / 'supplement'
|
|
shutil.copytree(source, directory)
|
|
base = root / 'base.json'
|
|
shutil.copyfile(ROOT / 'tests/results/source-archives/qt-sdk-notices-final/report.json', base)
|
|
report = json.loads((directory / 'report.json').read_text())
|
|
known = {row['resolvedPath']: row['sha256'] for row in report['runtimeComparisons']}
|
|
return directory, base, report, known
|
|
|
|
def test_fixed_supplement_preserves_all_six_original_files(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp); directory, base, report, known = self.supplement_inputs(root)
|
|
output = root / 'output'
|
|
package.copy_sdk_supplement(directory, base, known, output)
|
|
self.assertEqual(len(list(p for p in output.rglob('*') if p.is_file())), 7)
|
|
self.assertEqual(package.sha(output / 'report.json'), package.SDK_SUPPLEMENT_REPORT_SHA)
|
|
for row in report['files']:
|
|
self.assertEqual((output / row['file']).read_bytes(), (directory / row['file']).read_bytes())
|
|
|
|
def test_supplement_rejects_changed_source_report_base_runtime_and_symlink(self):
|
|
for change in ('source', 'report', 'base', 'runtime', 'symlink'):
|
|
with self.subTest(change=change), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp); directory, base, report, known = self.supplement_inputs(root)
|
|
source = directory / report['files'][0]['file']
|
|
if change == 'source': source.write_bytes(b'changed')
|
|
elif change == 'report': (directory / 'report.json').write_text('{}')
|
|
elif change == 'base': base.write_text('{}')
|
|
elif change == 'runtime': known[next(iter(known))] = '0' * 64
|
|
else:
|
|
outside = root / 'outside'; shutil.copyfile(source, outside)
|
|
source.unlink(); source.symlink_to(outside)
|
|
output = root / 'output'
|
|
with self.assertRaises(ValueError):
|
|
package.copy_sdk_supplement(directory, base, known, output)
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_destination_traversal_and_noncanonical_paths_rejected(self):
|
|
for value in ('../x', '/x', 'a/../b', '.', '', 'a//b', './x', 'x\\y', 'x\0y'):
|
|
with self.subTest(value=repr(value)), self.assertRaises(ValueError):
|
|
package.canonical(value)
|
|
self.assertEqual(str(package.canonical('qt/qml/QtQuick/qmldir')), 'qt/qml/QtQuick/qmldir')
|
|
|
|
def test_hash_mismatch_rejected_before_destination_created(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp); source = root / 'input'; source.write_bytes(b'actual')
|
|
target = root / 'output'
|
|
with self.assertRaisesRegex(ValueError, 'Input changed'):
|
|
package.copy_verified(source, target, '0' * 64)
|
|
self.assertFalse(target.exists())
|
|
|
|
def test_conflicting_alias_is_not_overwritten(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp); source = root / 'input'; source.write_bytes(b'first')
|
|
target = root / 'output'
|
|
package.copy_verified(source, target, executable=False)
|
|
package.copy_verified(source, target, executable=False)
|
|
source.write_bytes(b'different')
|
|
with self.assertRaisesRegex(ValueError, 'Conflicting'):
|
|
package.copy_verified(source, target)
|
|
self.assertEqual(target.read_bytes(), b'first')
|
|
|
|
def test_launcher_preserves_literal_arguments_and_selects_private_runtime(self):
|
|
with tempfile.TemporaryDirectory(prefix='docview launcher ') as tmp:
|
|
root = Path(tmp); app = root / 'app'; (app / 'bin').mkdir(parents=True)
|
|
binary = app / 'bin/docview'
|
|
binary.write_text('#!' + sys.executable + '\nimport os,sys,json\nprint(json.dumps({"argv":sys.argv[1:],"lib":os.environ["LD_LIBRARY_PATH"],"plugins":os.environ["QT_PLUGIN_PATH"],"qml":os.environ["QML_IMPORT_PATH"],"helper":os.environ["QTWEBENGINEPROCESS_PATH"]}))\n')
|
|
binary.chmod(0o755)
|
|
source = (ROOT / 'resources/linux/docview-launcher').read_text()
|
|
self.assertEqual(source.count('docview_root=/opt/docview'), 1)
|
|
launcher = root / 'launcher'
|
|
launcher.write_text(source.replace('docview_root=/opt/docview', 'docview_root=' + shlex.quote(str(app))))
|
|
literal = ['日本語 空白.pdf', 'a$(touch unexpected)', "quote'\";", '--config', 'a b.toml']
|
|
result = json.loads(subprocess.check_output(['sh', str(launcher), *literal], text=True,
|
|
env={**os.environ, 'LD_LIBRARY_PATH':'/invalid', 'QT_PLUGIN_PATH':'/invalid'}))
|
|
self.assertEqual(result['argv'], literal)
|
|
self.assertEqual(result['lib'], str(app / 'lib') + ':' + str(app / 'qt/lib'))
|
|
self.assertEqual(result['plugins'], str(app / 'qt/plugins'))
|
|
self.assertEqual(result['qml'], str(app / 'qt/qml'))
|
|
self.assertEqual(result['helper'], str(app / 'qt/libexec/QtWebEngineProcess'))
|
|
|
|
def test_maintainer_scripts_have_valid_shell_syntax(self):
|
|
for name in ('docview-launcher', 'deb-postinst', 'deb-prerm'):
|
|
subprocess.run(['sh', '-n', str(ROOT / 'resources/linux' / name)], check=True)
|
|
|
|
def test_staging_modes_do_not_depend_on_builder_umask(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp); regular = root / 'notice'; executable = root / 'launcher'
|
|
regular.write_text('text'); executable.write_text('text')
|
|
regular.chmod(0o666); executable.chmod(0o777); root.chmod(0o777)
|
|
package.normalize_modes(root)
|
|
self.assertEqual(regular.stat().st_mode & 0o7777, 0o644)
|
|
self.assertEqual(executable.stat().st_mode & 0o7777, 0o755)
|
|
self.assertEqual(root.stat().st_mode & 0o7777, 0o755)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|