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
+386
View File
@@ -0,0 +1,386 @@
#!/usr/bin/env python3
"""Portable manifest/copy tests. Synthetic MZ files do not test native PE scanning."""
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
SOURCE = Path(__file__).resolve().parents[1]
WRITER = SOURCE / "cmake/WriteWindowsRuntimeManifest.cmake"
STAGER = SOURCE / "cmake/StageWindowsWorker.cmake"
INSTALLER = SOURCE / "cmake/InstallWindowsWorker.cmake"
DEPLOYMENT = SOURCE / "cmake/WindowsDeployment.cmake"
CMAKE = shutil.which("cmake")
class RuntimeManifestTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="docview runtime 日本語 ")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.package = self.root / "package"
self.package.mkdir()
self.executable = self.file("package/worker.exe", b"MZworker")
self.manifest = Path(str(self.executable) + ".runtime.json")
def file(self, relative, data=b"MZfixture"):
path = self.root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
return path
def invoke(self, script, definitions=(), success=True):
command = [CMAKE, *[f"-D{name}={value}" for name, value in definitions], "-P", str(script)]
return self.run_command(command, success=success)
def run_command(self, command, success=True, env=None):
result = subprocess.run(command, capture_output=True, text=True, timeout=30, env=env)
if success:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
else:
self.assertNotEqual(result.returncode, 0, "Invalid package input was accepted")
return result
def write(self, files, success=True):
return self.invoke(WRITER, [("WORKER_EXECUTABLE", self.executable),
("RUNTIME_FILES", ";".join(map(str, files)))], success)
def stage(self, sources, success=True):
# Import the portable copy function without invoking the native scanner.
script = self.root / "copy.cmake"
script.write_text(f'include([==[{STAGER.as_posix()}]==])\n'
f'docview_stage_runtime_files([==[{self.executable.as_posix()}]==] '
f'[==[{";".join(path.as_posix() for path in sources)}]==])\n', encoding="utf-8")
return self.invoke(script, success=success)
def assert_inventory(self, files):
result = json.loads(self.manifest.read_text())
self.assertEqual(set(result), {"schemaVersion", "executable", "files"})
self.assertEqual(result["schemaVersion"], 1)
self.assertEqual(result["executable"], self.executable.name)
expected = {path.name: path for path in files}
self.assertEqual({entry["path"] for entry in result["files"]}, set(expected))
for entry in result["files"]:
self.assertEqual(set(entry), {"path", "size", "sha256"})
path = expected[entry["path"]]
self.assertIsInstance(entry["size"], int)
self.assertEqual(entry["size"], path.stat().st_size)
self.assertEqual(entry["sha256"], hashlib.sha256(path.read_bytes()).hexdigest())
self.assertRegex(entry["sha256"], r"^[0-9a-f]{64}$")
self.assertFalse(list(self.package.glob("*.tmp")))
def test_exact_schema_hashes_and_stable_order(self):
files = [self.file("package/Qt6Core.dll"), self.executable, self.file("package/zlib1.dll", b"MZother")]
self.write(files)
self.assert_inventory(files)
original = self.manifest.read_bytes()
self.write(list(reversed(files)))
self.assertEqual(self.manifest.read_bytes(), original)
def test_executable_required_and_failed_write_preserves_manifest(self):
self.write([self.executable])
original = self.manifest.read_bytes()
dll = self.file("package/one.dll")
self.write([dll], success=False)
self.assertEqual(self.manifest.read_bytes(), original)
self.write([], success=False)
self.assertEqual(self.manifest.read_bytes(), original)
def test_casefold_and_exact_duplicates_rejected(self):
upper = self.file("package/Example.dll")
lower = self.file("package/example.dll")
self.write([self.executable, upper, lower], success=False)
self.write([self.executable, upper, upper], success=False)
def test_invalid_names_and_file_kinds(self):
for name in ["CON.dll", "lpt9.dll", "aux.lib.dll", "trailing.dll.", "space name.dll",
"unexpected.exe", "notes.txt", ".hidden.dll", "a" * 125 + ".dll"]:
with self.subTest(name=name):
bad = self.file("package/" + name)
self.write([self.executable, bad], success=False)
def test_missing_non_mz_empty_directory_and_relative_entries(self):
directory = self.package / "directory.dll"
directory.mkdir()
for path in [self.package / "missing.dll", self.file("package/invalid.dll", b"not PE"),
self.file("package/empty.dll", b""), directory, Path("relative.dll")]:
with self.subTest(path=path):
self.write([self.executable, path], success=False)
def test_outside_directory_and_symlink_rejected(self):
external = self.file("outside/external.dll")
self.write([self.executable, external], success=False)
link = self.package / "link.dll"
try:
link.symlink_to(external)
except OSError as error:
self.skipTest(f"Cannot create a symlink on this host: {error}")
self.write([self.executable, link], success=False)
def test_manifest_destination_symlink_rejected(self):
protected = self.file("protected.txt", b"unchanged")
try:
self.manifest.symlink_to(protected)
except OSError as error:
self.skipTest(f"Cannot create a symlink on this host: {error}")
self.write([self.executable], success=False)
self.assertEqual(protected.read_bytes(), b"unchanged")
def test_128_files_allowed_129_rejected(self):
files = [self.executable] + [self.file(f"package/library{i}.dll") for i in range(127)]
self.write(files)
self.assert_inventory(files)
original = self.manifest.read_bytes()
self.write(files + [self.file("package/overflow.dll")], success=False)
self.assertEqual(self.manifest.read_bytes(), original)
def test_individual_and_total_size_limits_before_hashing(self):
first = self.file("package/large.dll")
with first.open("r+b") as stream:
stream.truncate(256 * 1024 * 1024 + 1)
self.write([self.executable, first], success=False)
with first.open("r+b") as stream:
stream.truncate(256 * 1024 * 1024)
second = self.file("package/second.dll")
with second.open("r+b") as stream:
stream.truncate(256 * 1024 * 1024)
self.write([self.executable, first, second], success=False)
self.assertFalse(self.manifest.exists())
def test_copy_and_manifest_are_idempotent(self):
first = self.file("input/One.dll")
duplicate = self.file("another/One.dll")
second = self.file("input/Two.dll", b"MZsecond")
self.stage([first, duplicate, first, second])
copied = [self.executable, self.package / first.name, self.package / second.name]
self.assert_inventory(copied)
original = self.manifest.read_bytes()
self.stage([second, first])
self.assert_inventory(copied)
self.assertEqual(self.manifest.read_bytes(), original)
self.assertEqual(first.read_bytes(), duplicate.read_bytes())
def test_copy_executable_without_dlls(self):
self.stage([])
self.assert_inventory([self.executable])
def test_os_filter_uses_exact_case_insensitive_directory_boundaries(self):
script = self.root / "os-filter.cmake"
script.write_text(f'include([==[{STAGER.as_posix()}]==])\n' + r'''
docview_regex_case_path("C:/Win(dows).old" escaped)
if(NOT "c:/WIN(dows).OLD/System32/kernel32.dll" MATCHES "^${escaped}/")
message(FATAL_ERROR "The OS filter did not preserve literal punctuation")
endif()
if("C:/WindowsXold/System32/kernel32.dll" MATCHES "^${escaped}/")
message(FATAL_ERROR "The OS filter treated a literal path as a regex")
endif()
foreach(path IN ITEMS "C:/Windows/System32/kernel32.dll" "c:/WINDOWS/SysWOW64/old.dll" "C:/Windows/OS.dll")
docview_is_windows_os_runtime("${path}" "C:/Windows/" result)
if(NOT result)
message(FATAL_ERROR "Windows OS dependency was not excluded: ${path}")
endif()
endforeach()
foreach(path IN ITEMS "C:/Windows-copy/System32/app.dll" "C:/Windows/System32-copy/app.dll" "D:/SDK/redist/runtime.dll")
docview_is_windows_os_runtime("${path}" "C:/Windows" result)
if(result)
message(FATAL_ERROR "Non-OS dependency was mistaken for an OS file: ${path}")
endif()
endforeach()
''', encoding="utf-8")
self.invoke(script)
def test_copy_conflicting_sources_and_case_rejected(self):
first = self.file("input/one.dll")
different = self.file("another/one.dll", b"MZdifferent")
self.stage([first, different], success=False)
self.assertFalse((self.package / first.name).exists())
case_variant = self.file("another/One.dll")
self.stage([first, case_variant], success=False)
self.assertFalse(self.manifest.exists())
def test_copy_existing_destination_is_never_overwritten(self):
first = self.file("input/one.dll")
original = self.file("package/one.dll", b"MZkeep")
self.write([self.executable, original])
manifest_before = self.manifest.read_bytes()
self.stage([first], success=False)
self.assertEqual(original.read_bytes(), b"MZkeep")
self.assertEqual(self.manifest.read_bytes(), manifest_before)
def test_copy_casefold_destination_conflict_rejected(self):
first = self.file("input/one.dll")
original = self.file("package/One.dll")
self.stage([first], success=False)
self.assertEqual(original.read_bytes(), first.read_bytes())
def test_copy_symlink_and_wrong_extension_rejected(self):
self.stage([self.file("input/source.txt")], success=False)
first = self.file("input/one.dll")
link = self.package / first.name
try:
link.symlink_to(first)
except OSError as error:
self.skipTest(f"Cannot create a symlink on this host: {error}")
self.stage([first], success=False)
self.assertEqual(first.read_bytes(), b"MZfixture")
def test_install_and_installed_file_tamper_detection(self):
dll = self.file("package/Qt6Core.dll")
self.write([self.executable, dll])
destination = self.root / "installed bin"
self.invoke(INSTALLER, [("WORKER_EXECUTABLE", self.executable),
("WORKER_INSTALL_DIRECTORY", destination)])
self.assertEqual((destination / dll.name).read_bytes(), dll.read_bytes())
installed = destination / self.executable.name
self.assertEqual(Path(str(installed) + ".runtime.json").read_bytes(), self.manifest.read_bytes())
self.invoke(INSTALLER, [("WORKER_EXECUTABLE", installed), ("WORKER_VERIFY_ONLY", "TRUE")])
(destination / dll.name).write_bytes(b"MZpatched")
self.invoke(INSTALLER, [("WORKER_EXECUTABLE", installed), ("WORKER_VERIFY_ONLY", "TRUE")], success=False)
def test_install_source_tamper_is_rejected_before_copy(self):
dll = self.file("package/Qt6Core.dll")
self.write([self.executable, dll])
dll.write_bytes(b"MZchanged")
destination = self.root / "installed"
self.invoke(INSTALLER, [("WORKER_EXECUTABLE", self.executable),
("WORKER_INSTALL_DIRECTORY", destination)], success=False)
self.assertFalse(destination.exists())
def test_installer_rejects_noncanonical_manifest_schema(self):
dll = self.file("package/Qt6Core.dll")
self.write([self.executable, dll])
original = self.manifest.read_bytes()
mutations = [
("extra-root-field", lambda value: value.update(extra=True)),
("string-version", lambda value: value.update(schemaVersion="1")),
("wrong-version", lambda value: value.update(schemaVersion=2)),
("wrong-worker", lambda value: value.update(executable="other.exe")),
("extra-entry-field", lambda value: value["files"][0].update(extra=True)),
("string-size", lambda value: value["files"][0].update(size=str(value["files"][0]["size"]))),
("fractional-size", lambda value: value["files"][0].update(size=3.5)),
("uppercase-hash", lambda value: value["files"][0].update(sha256=value["files"][0]["sha256"].upper())),
("traversal", lambda value: value["files"][0].update(path="../outside.dll")),
("duplicate-name", lambda value: value["files"].append(value["files"][0].copy())),
("empty-inventory", lambda value: value.update(files=[])),
("missing-worker", lambda value: value.update(files=[entry for entry in value["files"] if entry["path"].endswith(".dll")])),
]
for name, mutate in mutations:
with self.subTest(name=name):
value = json.loads(original)
mutate(value)
self.manifest.write_text(json.dumps(value))
self.invoke(INSTALLER, [("WORKER_EXECUTABLE", self.executable),
("WORKER_VERIFY_ONLY", "TRUE")], success=False)
def configure_synthetic_deployment(self):
# Configure the generated CMake glue using the host compiler and fake Qt
# targets. No synthetic MZ file is compiled or run as native machine code.
project = self.root / "glue"
project.mkdir()
build = self.root / "glue-build"
qt = self.file("sdk/bin/Qt6Core.dll", b"MZQtCore")
archive = self.file("sdk/bin/zip.dll", b"MZzip")
qpdf = self.file("qpdf/bin/qpdf.dll", b"MZqpdf")
trace = self.root / "deploy-arguments.json"
tool = self.file("fake-windeployqt", (f"#!{sys.executable}\n" +
"import json, os, pathlib, sys\n"
"args = sys.argv[1:]\n"
"assert '--nopatchqt' in args and '--no-compiler-runtime' in args\n"
"destination = pathlib.Path(args[args.index('--dir') + 1])\n"
f"pathlib.Path({str(trace)!r}).write_text(json.dumps(args))\n"
"if os.environ.get('DOCVIEW_MANIFEST_TEST_TAMPER') == '1':\n"
" (destination / 'Qt6Core.dll').write_bytes(b'MZmodified by deploy tool')\n").encode())
tool.chmod(0o700)
for name in ["docview-pdf-worker.exe", "docview-archive-worker.exe"]:
worker = self.file("package/" + name, b"MZ" + name.encode())
shutil.copyfile(qt, self.package / qt.name)
self.invoke(WRITER, [("WORKER_EXECUTABLE", worker),
("RUNTIME_FILES", f"{worker};{self.package / qt.name}")])
self.file("package/docview.exe", b"MZgui")
(project / "dummy.cpp").write_text("int main() { return 0; }\n")
text = f'''
cmake_minimum_required(VERSION 3.24)
project(WindowsDeploymentGlue LANGUAGES CXX)
set(WIN32 TRUE)
set(DOCVIEW_DUMPBIN_EXECUTABLE [==[{tool.as_posix()}]==] CACHE FILEPATH "")
set(DOCVIEW_WINDEPLOYQT_EXECUTABLE [==[{tool.as_posix()}]==] CACHE FILEPATH "")
set(Qt6_DIR [==[{(self.root / "sdk/lib/cmake/Qt6").as_posix()}]==])
set(DOCVIEW_PDFIUM_ROOT [==[{(self.root / "sdk").as_posix()}]==])
set(DOCVIEW_ZIP_TARGET MockZip)
add_library(Qt6::Core SHARED IMPORTED)
set_target_properties(Qt6::Core PROPERTIES IMPORTED_LOCATION [==[{qt.as_posix()}]==])
add_library(MockZip SHARED IMPORTED)
set_target_properties(MockZip PROPERTIES IMPORTED_LOCATION [==[{archive.as_posix()}]==])
add_library(qpdf::libqpdf SHARED IMPORTED)
set_target_properties(qpdf::libqpdf PROPERTIES IMPORTED_LOCATION [==[{qpdf.as_posix()}]==])
include([==[{DEPLOYMENT.as_posix()}]==])
foreach(name docview-pdf-worker docview-archive-worker docview)
add_executable(${{name}} dummy.cpp)
set_target_properties(${{name}} PROPERTIES SUFFIX ".exe" RUNTIME_OUTPUT_DIRECTORY [==[{self.package.as_posix()}]==])
endforeach()
docview_configure_windows_worker(docview-pdf-worker)
docview_install_windows_worker(docview-pdf-worker)
docview_install_windows_worker(docview-archive-worker)
install(FILES [==[{(self.package / "docview.exe").as_posix()}]==] DESTINATION bin)
docview_install_windows_gui(docview)
'''
(project / "CMakeLists.txt").write_text(text, encoding="utf-8")
self.run_command([CMAKE, "-S", str(project), "-B", str(build), "-DCMAKE_BUILD_TYPE=Release",
f"-DCMAKE_INSTALL_PREFIX={self.root / 'configure-prefix'}"])
return build, trace
@unittest.skipIf(os.name == "nt", "Portable glue probe uses a Unix executable Python stand-in; native deployment is separate")
def test_generated_glue_uses_runtime_prefix_and_single_destdir(self):
build, trace = self.configure_synthetic_deployment()
stage = (build / "stage-docview-pdf-worker-Release.cmake").read_text()
self.assertIn((self.package / "docview-pdf-worker.exe").as_posix(), stage)
self.assertNotIn("$<", stage)
self.assertIn("CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL dumpbin", stage)
self.assertIn((self.root / "qpdf/bin").as_posix(), stage)
install = (build / "install-docview-pdf-worker-Release.cmake").read_text()
self.assertIn('${CMAKE_INSTALL_PREFIX}/bin', install)
self.assertNotIn('$ENV{DESTDIR}', install)
prefix = self.root / "selected prefix"
destdir = self.root / "stage root"
environment = os.environ.copy()
environment["DESTDIR"] = str(destdir)
self.run_command([CMAKE, "--install", str(build), "--config", "Release", "--prefix", str(prefix)], env=environment)
actual = Path(str(destdir) + str(prefix)) / "bin"
self.assertTrue((actual / "docview-pdf-worker.exe").exists())
self.assertTrue((actual / "docview-archive-worker.exe").exists())
self.assertFalse(prefix.exists())
self.assertFalse((self.root / "configure-prefix").exists())
arguments = json.loads(trace.read_text())
self.assertEqual(arguments[arguments.index("--dir") + 1], str(actual))
self.assertIn("--nopatchqt", arguments)
self.assertIn("Plugins=plugins", (actual / "qt.conf").read_text())
@unittest.skipIf(os.name == "nt", "Portable glue probe uses a Unix executable Python stand-in; native deployment is separate")
def test_generated_glue_rejects_worker_dll_modified_by_deployment(self):
build, _ = self.configure_synthetic_deployment()
environment = os.environ.copy()
environment.pop("DESTDIR", None)
environment["DOCVIEW_MANIFEST_TEST_TAMPER"] = "1"
prefix = self.root / "tampered install"
result = self.run_command([CMAKE, "--install", str(build), "--config", "Release", "--prefix", str(prefix)],
success=False, env=environment)
self.assertIn("no longer matches", result.stderr)
@unittest.skipIf(sys.platform == "win32", "This assertion is for unsupported scanner hosts")
def test_non_windows_native_scanner_fails_explicitly(self):
result = self.invoke(STAGER, [("WORKER_EXECUTABLE", self.executable)], success=False)
self.assertIn("requires a Windows host", result.stderr)
self.assertFalse(self.manifest.exists())
if __name__ == "__main__":
if not CMAKE:
raise SystemExit("cmake is required")
unittest.main()