66 lines
4.1 KiB
Python
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)
|