#!/usr/bin/env python3 """Source-level invariants, not a substitute for Android lint, Gradle or device testing.""" from pathlib import Path import xml.etree.ElementTree as ET root = Path(__file__).resolve().parent.parent android = '{http://schemas.android.com/apk/res/android}' for xml in (root / 'androidApp/src').rglob('*.xml'): ET.parse(xml) manifest = ET.parse(root / 'androidApp/src/main/AndroidManifest.xml').getroot() assert not manifest.findall('uses-permission'), 'Unexpected permission in app manifest' application = manifest.find('application') assert application.get(android+'allowBackup') == 'false' service = application.find('service') assert service.get(android+'isolatedProcess') == 'true' assert service.get(android+'exported') == 'false' provider = application.find('provider') assert provider.get(android+'exported') == 'false' for source in (root / 'core/src/commonMain').rglob('*.kt'): content = source.read_text() assert 'import android.' not in content and 'import java.' not in content, source all_android = '\n'.join(p.read_text() for p in (root / 'androidApp/src/main/kotlin').rglob('*.kt')) assert 'WebView(' not in all_android assert 'Log.' not in all_android print('PASS project invariants: XML, permission-free manifest, isolated PDF service, narrow FileProvider, portable core, no WebView/log calls.') # Browser/picker entry points must not impersonate the system document chooser. activities = {item.get(android+'name'): item for item in application.findall('activity')} main = activities['.MainActivity'] picker = activities['.FilePickerActivity'] assert picker.get(android+'exported') == 'true' assert picker.get(android+'launchMode', 'standard') == 'standard' all_actions = {item.get(android+'name') for item in application.findall('.//action')} assert 'android.intent.action.GET_CONTENT' in all_actions assert 'android.intent.action.OPEN_DOCUMENT' not in all_actions assert 'android.intent.action.OPEN_DOCUMENT_TREE' not in all_actions assert any(item.get(android+'name') == 'android.intent.category.APP_FILES' for item in main.findall('.//category')) assert any(item.get(android+'name') == 'android.intent.category.OPENABLE' for item in picker.findall('.//category')) paths = ET.parse(root / 'androidApp/src/main/res/xml/file_paths.xml').getroot() assert {(item.tag, item.get('path')) for item in paths} == {('cache-path', 'shared/'), ('cache-path', 'picked/')} assert all('FixtureDocumentsProvider' not in item.get(android+'name', '') for item in application.findall('provider')) for file in ['MainActivity.kt', 'FilePickerActivity.kt']: content = (root / 'androidApp/src/main/kotlin/dev/fileworkbench/android' / file).read_text() assert 'sanitized' in content assert content.index('sanitized') < content.index('super.onCreate(savedInstanceState)') copy_code = (root / 'androidApp/src/main/kotlin/dev/fileworkbench/android/browser/PickedFileStore.kt').read_text() assert 'SelectionCopyBudget()' in copy_code assert 'flags = Intent.FLAG_GRANT_READ_URI_PERMISSION' in copy_code assert 'FLAG_GRANT_PREFIX_URI_PERMISSION' not in copy_code assert 'FLAG_GRANT_WRITE_URI_PERMISSION' not in copy_code assert 'FLAG_GRANT_PERSISTABLE_URI_PERMISSION' not in copy_code print('PASS browser source invariants: APP_FILES/GET_CONTENT, standard picker activity, no system-picker impersonation, narrow copied paths, read-only results, sanitized activity defaults, debug fixture excluded from main manifest.') # Regression guard for the reported instrumentation compile failure. This deliberately # checks only the known constructor/use pattern; it is NOT Kotlin type checking. import re for source in (root / 'androidApp/src').rglob('*.kt'): assert not re.search(r"\bPdfDocument\s*\(\s*\)\s*\.\s*use\b", source.read_text()), ( f'{source}: PdfDocument does not implement Closeable/AutoCloseable; use try/finally' ) print('PASS PDF source regression guard: no PdfDocument().use call (pattern check only).') # The release is intentionally pinned; prevent mixed old/new wrapper or plugin files. import subprocess import sys subprocess.run([sys.executable, str(root / 'scripts/check-build-config.py')], check=True)