mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
105 lines
4.9 KiB
Python
105 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline APK/JNI export regressions using real, stripped ELF fixtures.
|
|
|
|
Requires a host C compiler and readelf; no Android SDK or device is needed.
|
|
The fixture functions test symbol visibility only, not the callbacks' ABI.
|
|
"""
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
|
|
sys.dont_write_bytecode = True
|
|
spec = importlib.util.spec_from_file_location(
|
|
"native", Path(__file__).with_name("verify-android-native.py")
|
|
)
|
|
native = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(native)
|
|
|
|
|
|
class JniExportsTests(unittest.TestCase):
|
|
def build_library(self, directory, replacement=None, stripped=True):
|
|
source = Path(directory) / "fixture.c"
|
|
library = Path(directory) / "fixture.so"
|
|
source.write_text("\n".join(
|
|
replacement[1] if replacement and name == replacement[0]
|
|
else f"void {name}(void) {{}}"
|
|
for name in sorted(native.REQUIRED_EXPORTS)
|
|
))
|
|
command = ["cc", "-shared", "-fPIC", "-Wl,-z,max-page-size=16384"]
|
|
if stripped:
|
|
command.append("-s")
|
|
subprocess.run(command + [str(source), "-o", str(library)],
|
|
check=True, capture_output=True, text=True)
|
|
return library.read_bytes()
|
|
|
|
def test_every_native_declaration_is_required(self):
|
|
activity = (Path(__file__).resolve().parents[1] / "platform/android/gradle/app/src/main"
|
|
/ "java/com/flectar/mail/FlectarActivity.java")
|
|
declared = set(re.findall(r"\bnative\s+\w+\s+(\w+)\s*\(", activity.read_text()))
|
|
prefix = "Java_com_flectar_mail_FlectarActivity_"
|
|
required = {name.removeprefix(prefix) for name in native.REQUIRED_EXPORTS
|
|
if name.startswith(prefix)}
|
|
self.assertEqual(declared, required)
|
|
|
|
def test_release_stripping_preserves_required_dynamic_exports(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
data = self.build_library(directory)
|
|
native.verify_elf(data, "fixture.so")
|
|
native.verify_jni_exports(data, "fixture.so")
|
|
|
|
def test_each_missing_callback_and_entry_point_is_rejected(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
for name in sorted(native.REQUIRED_EXPORTS):
|
|
with self.subTest(symbol=name):
|
|
data = self.build_library(directory, (name, ""))
|
|
with self.assertRaisesRegex(ValueError, name):
|
|
native.verify_jni_exports(data, "fixture.so")
|
|
|
|
def test_non_callable_symbols_do_not_satisfy_the_contract(self):
|
|
name = "Java_com_flectar_mail_FlectarActivity_nativeGoogleAuthorizationResult"
|
|
variants = {
|
|
"hidden": f'__attribute__((visibility("hidden"))) void {name}(void) {{}}',
|
|
"local": f"static void {name}(void) {{}}",
|
|
"undefined": f"extern void {name}(void); void call_missing(void) {{ {name}(); }}",
|
|
"data": f"int {name} = 1;",
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
for kind, source in variants.items():
|
|
with self.subTest(kind=kind):
|
|
# Retain debug/static symbols to ensure they cannot mask a
|
|
# missing callable symbol in the dynamic table.
|
|
data = self.build_library(directory, (name, source), stripped=False)
|
|
with self.assertRaisesRegex(ValueError, name):
|
|
native.verify_jni_exports(data, "fixture.so")
|
|
|
|
def test_apk_checks_each_packaged_abi(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
complete = self.build_library(directory)
|
|
name = "Java_com_flectar_mail_FlectarActivity_nativeGoogleAuthorizationResult"
|
|
incomplete = self.build_library(directory, (name, ""))
|
|
apk = Path(directory) / "fixture.apk"
|
|
for second_library in (complete, incomplete):
|
|
with zipfile.ZipFile(apk, "w") as archive:
|
|
for abi, app in (("arm64-v8a", complete), ("x86_64", second_library)):
|
|
# Host ELF fixtures represent each packaging location;
|
|
# they are never executed as Android code.
|
|
archive.writestr(f"lib/{abi}/libflectar_mail_android.so", app)
|
|
archive.writestr(f"lib/{abi}/libpdfium.so", complete)
|
|
notices = f"assets/licenses/PDFium/{abi}/pdfium-licenses"
|
|
archive.writestr(f"{notices}/build.json", "{}")
|
|
archive.writestr(f"{notices}/LICENSE", "fixture")
|
|
if second_library == complete:
|
|
native.verify_apk(apk)
|
|
else:
|
|
with self.assertRaisesRegex(ValueError, f"x86_64/.*{name}"):
|
|
native.verify_apk(apk)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|