initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
#!/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()