Files
zemion db873063f8
Verify source and build Android / android (push) Canceled after 0s
initial commit v 0.2.0
2026-09-24 22:49:27 +02:00

66 lines
4.1 KiB
Python

#!/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)