mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-17 18:37:14 +00:00
173 lines
9.4 KiB
Python
173 lines
9.4 KiB
Python
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from xml.sax.saxutils import escape
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
import bundle
|
|
|
|
|
|
class BundleTests(unittest.TestCase):
|
|
def test_missing_bitmap_scaling_rules_fail_packaging(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
with self.assertRaisesRegex(RuntimeError, "bitmap scaling rules not found"):
|
|
bundle.write_fontconfig(directory / "Resources", directory / "missing-prefix")
|
|
|
|
@unittest.skipUnless(shutil.which("fc-match"), "requires Fontconfig's matching tool")
|
|
def test_bundled_bitmap_scaling_survives_relocation(self):
|
|
prefix = Path(bundle.run("brew", "--prefix", "fontconfig")) if sys.platform == "darwin" else Path("/usr")
|
|
rule = prefix / "share/fontconfig/conf.avail/10-scale-bitmap-fonts.conf"
|
|
if not rule.is_file():
|
|
self.skipTest("Fontconfig bitmap scaling configuration unavailable")
|
|
font = bundle.ROOT / "data/fonts/NotoColorEmoji.ttf"
|
|
with tempfile.TemporaryDirectory(prefix="zoitechat font scaling ") as temporary:
|
|
directory = Path(temporary)
|
|
source = directory / "build-prefix/share/fontconfig/conf.avail" / rule.name
|
|
source.parent.mkdir(parents=True)
|
|
shutil.copy2(rule, source)
|
|
resources = directory / "Resources"
|
|
bundle.write_fontconfig(resources, directory / "build-prefix")
|
|
relocated = directory / "Relocated Resources"
|
|
shutil.move(resources, relocated)
|
|
source.unlink()
|
|
# Exercise the shipped configuration without consulting host font
|
|
# rules. Only the bundled font is visible after reset-dirs.
|
|
wrapper = directory / "probe.conf"
|
|
wrapper.write_text(
|
|
'<fontconfig><include>' + escape(str(relocated / "fonts.conf")) +
|
|
'</include><reset-dirs/><dir>' + escape(str(font.parent)) + '</dir></fontconfig>')
|
|
env = os.environ.copy()
|
|
env["FONTCONFIG_FILE"] = str(wrapper)
|
|
env["XDG_CACHE_HOME"] = str(directory / "cache")
|
|
for requested in (12, 16, 24):
|
|
output = subprocess.check_output(
|
|
["fc-match", "-f", "%{pixelsize}\\n%{matrix}\\n%{file}\\n",
|
|
f"Noto Color Emoji:pixelsize={requested}"], env=env, cwd=directory, text=True)
|
|
pixels, matrix, matched = output.splitlines()
|
|
self.assertEqual(Path(matched).resolve(), font.resolve())
|
|
values = [float(value) for value in matrix.split()]
|
|
self.assertEqual(len(values), 4, "bitmap scaling matrix missing")
|
|
self.assertAlmostEqual(float(pixels) * values[0], requested, places=3)
|
|
self.assertAlmostEqual(float(pixels) * values[3], requested, places=3)
|
|
self.assertEqual(values[1:3], [0.0, 0.0])
|
|
|
|
@unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
|
|
def test_signed_binary_relocation_and_resigning(self):
|
|
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary:
|
|
directory = Path(temporary)
|
|
library_source = directory / "answer.c"
|
|
library_source.write_text("int answer(void) { return 42; }\n")
|
|
library = directory / "libanswer.dylib"
|
|
main_source = directory / "main.c"
|
|
main_source.write_text(
|
|
"#include <stdio.h>\n"
|
|
"extern int answer(void);\n"
|
|
"int main(void) { printf(\"%d\\n\", answer()); return 0; }\n"
|
|
)
|
|
executable = directory / "original"
|
|
bundle.run("xcrun", "clang", "-dynamiclib", "-Wl,-headerpad_max_install_names",
|
|
"-Wl,-install_name," + str(library), library_source, "-o", library)
|
|
bundle.run("xcrun", "clang", "-Wl,-headerpad_max_install_names",
|
|
"-Wl,-rpath," + str(directory), main_source, library, "-o", executable)
|
|
for binary in (library, executable):
|
|
bundle.run("codesign", "--force", "--sign", "-", binary)
|
|
app = directory / "Test.app"
|
|
target = app / "Contents/MacOS/zoitechat"
|
|
target.parent.mkdir(parents=True)
|
|
shutil.copy2(executable, target)
|
|
relocator = bundle.Relocator(app, executable)
|
|
relocator.relocate(executable, target)
|
|
# Remove the original dependency so execution proves relocation.
|
|
library.unlink()
|
|
for binary in (app / "Contents/Frameworks/libanswer.dylib", target):
|
|
bundle.run("codesign", "--force", "--sign", "-", binary)
|
|
bundle.run("codesign", "--verify", "--strict", binary)
|
|
self.assertEqual(bundle.run(target), "42")
|
|
self.assertEqual(len(bundle.audit(app)["mach_o"]), 2)
|
|
|
|
def test_otool_preserves_paths_with_spaces(self):
|
|
output = "app:\n\t/opt/Build Dependencies/lib/liba.dylib (compatibility version 1.0.0, current version 1.0.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1.0.0)"
|
|
with patch.object(bundle, "run", return_value=output):
|
|
self.assertEqual(bundle.dependencies(Path("app")), ["/opt/Build Dependencies/lib/liba.dylib", "/usr/lib/libSystem.B.dylib"])
|
|
|
|
def test_lc_rpaths_preserve_spaces(self):
|
|
with patch.object(bundle, "run", return_value="cmd LC_RPATH\n cmdsize 48\n path @loader_path/../Build Dependencies (offset 12)\n"):
|
|
self.assertEqual(bundle.rpaths(Path("app")), ["@loader_path/../Build Dependencies"])
|
|
|
|
def test_inherited_rpath_resolution(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
library = directory / "with spaces/libtest.dylib"
|
|
library.parent.mkdir()
|
|
library.write_bytes(b"library")
|
|
self.assertEqual(bundle.resolve_dependency("@rpath/libtest.dylib", directory / "libparent.dylib", directory / "app", [library.parent]), library.resolve())
|
|
|
|
def test_unresolved_rpath_fails(self):
|
|
with self.assertRaisesRegex(RuntimeError, "Cannot resolve"):
|
|
bundle.resolve_dependency("@rpath/missing.dylib", Path("/tmp/parent"), Path("/tmp/app"), [])
|
|
|
|
def test_conflicting_basename_fails(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
sources = [directory / name / "libtest.dylib" for name in ("one", "two")]
|
|
for source in sources:
|
|
source.parent.mkdir()
|
|
source.write_bytes(b"library")
|
|
relocator = bundle.Relocator(directory / "App.app", directory / "executable")
|
|
relocator.copy_dependency(sources[0])
|
|
with self.assertRaisesRegex(RuntimeError, "Conflicting library basename"):
|
|
relocator.copy_dependency(sources[1])
|
|
|
|
def test_audit_rejects_external_symlink(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
app = Path(temporary) / "App.app"
|
|
app.mkdir()
|
|
(app / "outside").symlink_to("/usr/lib")
|
|
with patch.object(bundle, "run", return_value="arm64"), self.assertRaisesRegex(RuntimeError, "External symlink"):
|
|
bundle.audit(app)
|
|
|
|
def test_audit_raises_minimum_for_dependencies(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
app = Path(temporary) / "App.app"
|
|
binary = app / "Contents/MacOS/zoitechat"
|
|
library = app / "Contents/Frameworks/libtest.dylib"
|
|
for path in (binary, library):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(bytes.fromhex("cffaedfe") + b"payload")
|
|
def fake_run(*args):
|
|
if args[0] == "lipo":
|
|
return "arm64"
|
|
if args[1] == "-D":
|
|
return str(args[2]) + ("\n@rpath/libtest.dylib" if args[2] == library else "")
|
|
if args[1] == "-L":
|
|
name = "@loader_path/../Frameworks/libtest.dylib" if args[2] == binary else "@rpath/libtest.dylib"
|
|
return f"{args[2]}:\n\t{name} (compatibility version 1.0.0, current version 1.0.0)"
|
|
return "cmd LC_BUILD_VERSION\n cmdsize 32\n platform 1\n minos " + ("15.0.1" if args[2] == library else "11.0")
|
|
with patch.object(bundle, "run", side_effect=fake_run):
|
|
report = bundle.audit(app)
|
|
self.assertEqual(report["minimum_macos"], "15.0.1")
|
|
self.assertEqual(report["architectures"], ["arm64"])
|
|
self.assertEqual(len(report["mach_o"]), 2)
|
|
|
|
def test_audit_rejects_wrong_architecture(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
app = Path(temporary) / "App.app"
|
|
binary = app / "Contents/MacOS/zoitechat"
|
|
library = app / "Contents/Frameworks/libtest.dylib"
|
|
for path in (binary, library):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(bytes.fromhex("cffaedfe"))
|
|
with patch.object(bundle, "run", side_effect=["arm64", "x86_64"]), self.assertRaisesRegex(RuntimeError, "Architecture mismatch"):
|
|
bundle.audit(app)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|