mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
fix(ci): audit stripped iOS archives and configure FFmpeg tests
This commit is contained in:
@@ -8,7 +8,7 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Sequence, Set, Tuple
|
||||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
|
||||
MACHO_MAGICS = {
|
||||
@@ -140,7 +140,26 @@ def defined_symbol(nm_output: str, marker: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def audit(app: Path, backend: str, archs: Sequence[str], platform: str, release: bool) -> str:
|
||||
def macho_uuids(path: Path) -> Dict[str, str]:
|
||||
output = run_xcrun("dwarfdump", "--uuid", str(path))
|
||||
matches = re.findall(r"UUID: ([0-9A-Fa-f-]{36}) \(([^)]+)\)", output)
|
||||
uuids = {arch: uuid.upper() for uuid, arch in matches}
|
||||
if not uuids or len(uuids) != len(matches):
|
||||
raise AuditError("missing or duplicate Mach-O UUIDs: " + str(path))
|
||||
return uuids
|
||||
|
||||
|
||||
def read_dsym_symbols(executable: Path, dsym: Path) -> str:
|
||||
dwarf = dsym / "Contents" / "Resources" / "DWARF" / executable.name
|
||||
if dwarf.is_symlink() or not dwarf.is_file() or not is_macho(dwarf):
|
||||
raise AuditError("missing Mach-O dSYM for main executable: " + str(dwarf))
|
||||
if macho_uuids(executable) != macho_uuids(dwarf):
|
||||
raise AuditError("dSYM UUIDs/architectures do not match the main executable")
|
||||
return run_xcrun("nm", "-gU", str(dwarf))
|
||||
|
||||
|
||||
def audit(app: Path, backend: str, archs: Sequence[str], platform: str, release: bool,
|
||||
dsym: Optional[Path] = None) -> str:
|
||||
if not app.is_dir():
|
||||
raise AuditError(".app is not a directory: " + str(app))
|
||||
executable = read_main_executable(app)
|
||||
@@ -173,6 +192,12 @@ def audit(app: Path, backend: str, archs: Sequence[str], platform: str, release:
|
||||
nm = run_xcrun("nm", "-gU", str(path))
|
||||
macho.append((relative.as_posix(), otool, nm))
|
||||
|
||||
# Archive strips static Rust symbols from Runner after generating its dSYM.
|
||||
# Only accept symbol evidence from the exact same executable (all UUIDs and
|
||||
# architectures must match); continue scanning the shipped bundle for Go.
|
||||
if dsym is not None:
|
||||
macho.append(("main executable dSYM", "", read_dsym_symbols(executable, dsym)))
|
||||
|
||||
go_section = any(section in otool for _, otool, _ in macho for section in GO_SECTIONS)
|
||||
go_symbol = any(
|
||||
defined_symbol(nm, symbol) for _, _, nm in macho for symbol in GO_SYMBOLS
|
||||
@@ -199,6 +224,8 @@ def make_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--archs", required=True, metavar="ARCH[,ARCH...]")
|
||||
parser.add_argument("--platform", choices=tuple(PLATFORM_NAMES), required=True)
|
||||
parser.add_argument("--release", action="store_true", help="apply release artifact checks")
|
||||
parser.add_argument("--dsym", type=Path,
|
||||
help="matching main executable .dSYM for a stripped archive")
|
||||
return parser
|
||||
|
||||
|
||||
@@ -206,7 +233,7 @@ def main(argv: Sequence[str] = None) -> int:
|
||||
args = make_parser().parse_args(argv)
|
||||
try:
|
||||
archs = parse_archs(args.archs)
|
||||
digest = audit(args.app, args.backend, archs, args.platform, args.release)
|
||||
digest = audit(args.app, args.backend, archs, args.platform, args.release, args.dsym)
|
||||
except (AuditError, OSError) as exc:
|
||||
print("error: " + str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Regression coverage for backend evidence in stripped iOS archives."""
|
||||
|
||||
import plistlib
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import check_backend_ios as checker
|
||||
|
||||
|
||||
class BackendArchiveAuditTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
root = Path(directory.name)
|
||||
self.app = root / "Runner.app"
|
||||
self.app.mkdir()
|
||||
with (self.app / "Info.plist").open("wb") as stream:
|
||||
plistlib.dump({"CFBundleExecutable": "Runner"}, stream)
|
||||
self.binary = self.app / "Runner"
|
||||
self.binary.write_bytes(b"\xcf\xfa\xed\xfe" + b"fixture")
|
||||
self.dsym = root / "Runner.app.dSYM"
|
||||
self.dwarf = self.dsym / "Contents/Resources/DWARF/Runner"
|
||||
self.dwarf.parent.mkdir(parents=True)
|
||||
self.dwarf.write_bytes(b"\xcf\xfa\xed\xfe" + b"symbols")
|
||||
self.binary_symbols = ""
|
||||
self.dsym_symbols = "0000000100010000 T _uniffi_spotiflac_mobile_fn_func_probe\n"
|
||||
self.dsym_uuid = "12345678-1234-1234-1234-123456789ABC"
|
||||
self.dsym_arch = "arm64"
|
||||
self.sections = ""
|
||||
mock = patch.object(checker, "run_xcrun", side_effect=self.xcrun)
|
||||
mock.start()
|
||||
self.addCleanup(mock.stop)
|
||||
|
||||
def xcrun(self, tool, *args):
|
||||
if tool == "lipo":
|
||||
return "arm64\n"
|
||||
if tool == "vtool":
|
||||
return " platform IOS\n"
|
||||
if tool == "otool":
|
||||
return self.sections
|
||||
if tool == "nm":
|
||||
return self.dsym_symbols if Path(args[-1]) == self.dwarf else self.binary_symbols
|
||||
if tool == "dwarfdump":
|
||||
if Path(args[-1]) == self.dwarf:
|
||||
return f"UUID: {self.dsym_uuid} ({self.dsym_arch}) {self.dwarf}\n"
|
||||
return f"UUID: 12345678-1234-1234-1234-123456789ABC (arm64) {self.binary}\n"
|
||||
self.fail("Unexpected xcrun tool: " + tool)
|
||||
|
||||
def audit(self, dsym=None):
|
||||
return checker.audit(self.app, "rust", ("arm64",), "ios", True, dsym)
|
||||
|
||||
def test_unstripped_binary_needs_no_dsym(self):
|
||||
self.binary_symbols = self.dsym_symbols
|
||||
self.assertEqual(self.audit(), checker.sha256(self.binary))
|
||||
|
||||
def test_stripped_binary_requires_symbol_evidence(self):
|
||||
with self.assertRaisesRegex(checker.AuditError, "no defined"):
|
||||
self.audit()
|
||||
|
||||
def test_matching_dsym_verifies_stripped_binary(self):
|
||||
self.assertEqual(self.audit(self.dsym), checker.sha256(self.binary))
|
||||
|
||||
def test_dsym_from_another_build_is_rejected(self):
|
||||
self.dsym_uuid = "ABCDEF01-1234-1234-1234-123456789ABC"
|
||||
with self.assertRaisesRegex(checker.AuditError, "do not match"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_dsym_from_another_architecture_is_rejected(self):
|
||||
self.dsym_arch = "x86_64"
|
||||
with self.assertRaisesRegex(checker.AuditError, "do not match"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_missing_dsym_is_rejected(self):
|
||||
self.dwarf.unlink()
|
||||
with self.assertRaisesRegex(checker.AuditError, "missing Mach-O dSYM"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_matching_dsym_without_defined_rust_is_rejected(self):
|
||||
self.dsym_symbols = " U _uniffi_spotiflac_mobile_fn_func_probe\n"
|
||||
with self.assertRaisesRegex(checker.AuditError, "no defined"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_matching_dsym_does_not_hide_go_in_shipped_binary(self):
|
||||
self.sections = "sectname __gopclntab\n"
|
||||
with self.assertRaisesRegex(checker.AuditError, "contains Go"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_matching_dsym_with_go_symbols_is_rejected(self):
|
||||
self.dsym_symbols += "0000000100020000 T _crosscall2\n"
|
||||
with self.assertRaisesRegex(checker.AuditError, "contains Go"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
def test_debug_artifacts_remain_forbidden_with_dsym(self):
|
||||
(self.app / "kernel_blob.bin").write_bytes(b"fixture")
|
||||
with self.assertRaisesRegex(checker.AuditError, "forbidden kernel_blob.bin"):
|
||||
self.audit(self.dsym)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user