"""Import from my Quest (ui/frame_quest.py) against a fake adb (tests/fakeadb.py), and split APK merging in the shared install path (frame_android.install(splits=...)).""" import sandbox # noqa: F401 (first: keeps tests off real data and services) import io import json import os from pathlib import Path from types import SimpleNamespace import struct import sys import tempfile import unittest from unittest.mock import patch import zipfile import zlib ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / 'ui')) import frame_android as android # noqa: E402 import frame_apk # noqa: E402 import frame_apk_sign # noqa: E402 import frame_apk_vr # noqa: E402 import frame_quest as quest # noqa: E402 from apk_fixtures import pool # noqa: E402 FAKEADB = str(Path(__file__).resolve().parent / 'fakeadb.py') PKG = 'com.example.questgame' QUEST = {'serial': '1WMHH000ABCDEF', 'state': 'device', 'extra': 'usb:1-1 product:eureka model:Quest_3 transport_id:2', 'props': {'ro.product.manufacturer': 'Oculus', 'ro.product.model': 'Quest 3'}} TUNNEL = {'serial': '127.0.0.1:40123', 'state': 'device', 'extra': 'product:lepton model:Lepton transport_id:1', 'props': {'ro.product.manufacturer': 'Valve', 'ro.product.model': 'Lepton'}} PHONE = {'serial': 'R58M000PHONE', 'state': 'device', 'extra': 'usb:1-2 model:SM_G990 transport_id:3', 'props': {'ro.product.manufacturer': 'samsung', 'ro.product.model': 'SM-G990'}} def axml(package, split=None, split_required=False, resmap=False, feature=False, children=(), version_code=None, version_name=None): """A minimal binary AndroidManifest.xml: children. resmap: name isSplitRequired by its android resource id (as real builds do), not only by name.""" strings = ['isSplitRequired', 'package', 'split', 'manifest', 'application', 'label', package, split or '', 'Quest Game', 'isFeatureSplit', 'versionCode', 'versionName', version_name or '', *children] none = 0xffffffff def start(tag, attrs): body = struct.pack('') self.refused(apk_bytes(split='config.arm64_v8a', children=('uses-library',), files=OPENXR), '') self.refused(apk_bytes(split='feature_vr', feature=True, files=OPENXR), 'feature split') self.refused(apk_bytes(split='config.arm64_v8a', files={'kotlin/x.kotlin_builtins': b''}), 'contains kotlin/') self.refused(apk_bytes(split='config.arm64_v8a', files={'lib/arm64-v8a/sub/libx.so': b''}), 'contains lib/') def test_resource_tables(self): from apk_fixtures import resources self.refused(apk_bytes(split='config.en', files={'resources.arsc': resources({(1, 'en', 0): {0: 1}})}), 'density or language') base, empty = self.files(apk_bytes(split_required=True), apk_bytes(split='config.arm64_v8a', files=dict(OPENXR, **{'resources.arsc': resources({})}))) self.assertEqual(set(android.split_entries(base, [empty])), set(OPENXR)) def test_zip_bomb_refused_by_headers_and_by_reads(self): bomb = {'assets/bomb.bin': b'\0' * (8 * 1024**2)} split = apk_bytes(split='install_time', files=bomb, deflate=True) self.assertLess(len(split), 64 * 1024) # tiny on disk base, part = self.files(apk_bytes(split_required=True), split) with patch.object(android, 'MAX_SPLIT_MEMBER', 1024**2): with self.assertRaisesRegex(android.FrameError, 'more than Frame Control merges'): android.split_entries(base, [part]) with patch.object(android, 'MAX_SPLIT_TOTAL', 4 * 1024**2): with self.assertRaisesRegex(android.FrameError, 'more than 0 GB|more than Frame Control merges'): android.split_entries(base, [part]) # Headers that understate the size: the read stops at what they promise, and the # truncated member fails its checksum instead of filling memory. base, forged = self.files(apk_bytes(split_required=True), lie_about_size(split, 'assets/bomb.bin', 1000)) self.assertEqual(android.split_size([forged]), 1000) with patch.object(android, 'repack') as repack: with self.assertRaises(android.FrameError): android.merge_splits(base, [forged], os.path.join(os.path.dirname(base), 'out.apk')) repack.assert_not_called() class SplitForgeries(unittest.TestCase): """Members whose headers understate their size, with a checksum that matches the prefix.""" def merge(self, split): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) base, part = os.path.join(tmp.name, 'base.apk'), os.path.join(tmp.name, 'split.apk') with open(base, 'wb') as f: f.write(apk_bytes(split_required=True)) with open(part, 'wb') as f: f.write(split) with patch.object(android, 'repack') as repack: with self.assertRaisesRegex(android.FrameError, 'does not match its header'): android.merge_splits(base, [part], os.path.join(tmp.name, 'out.apk')) repack.assert_not_called() def test_stored_and_deflated_truncation_refused(self): content = bytes(range(256)) * 64 # 16 KB prefix = zlib.crc32(content[:1000]) for deflate in (False, True): split = apk_bytes(split='install_time', deflate=deflate, files={'assets/level.pak': content}) forged = lie_about_size(split, 'assets/level.pak', 1000, crc=prefix) # Python's own reader is fooled: it stops at the declared size and the prefix checksum matches. with zipfile.ZipFile(io.BytesIO(forged)) as z: self.assertEqual(z.read('assets/level.pak'), content[:1000]) self.merge(forged) def test_honest_members_still_merge(self): content = bytes(range(256)) * 64 tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) for deflate in (False, True): part = os.path.join(tmp.name, f'split{deflate}.apk') with open(part, 'wb') as f: f.write(apk_bytes(split='install_time', deflate=deflate, files={'assets/level.pak': content})) self.assertEqual(android.read_member(part, 'assets/level.pak'), content) def test_bag_only_resource_table_is_not_empty(self): from apk_fixtures import resources simple = resources({(1, '', 0): {0: 1}}) entry = struct.pack('