"""frame_apk against a small APK built here: binary manifest plus resource table.""" import io import os import struct import sys import tempfile import tracemalloc import unittest import zipfile sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ui')) import frame_apk # noqa: E402 def pool(strings, utf8=False): """A ResStringPool chunk.""" data, offsets = b'', [] for s in strings: offsets.append(len(data)) if utf8: b = s.encode() data += bytes([len(s), len(b)]) + b + b'\0' else: data += struct.pack('. package_raw=False drops the package's raw string (as some repackers do); foreign_label adds a non-android `label` attribute after android:label. """ strings = ['label', 'icon', 'versionName', 'minSdkVersion', 'package', 'manifest', 'uses-sdk', 'application', package, 'junk', 'label'] # the second 'label' has no android id resmap = struct.pack('<4I', 0x01010001, 0x01010002, 0x0101021c, 0x0101020c) resmap = struct.pack('', 'lib/arm64-v8a/libx.so': b'', 'lib/x86_64/libx.so': b'', })) self.assertEqual(info['package'], 'com.example.demo') self.assertEqual(info['label'], 'App label') # the default, not French self.assertEqual(info['version'], '2.1') self.assertEqual(info['min_sdk'], 26) self.assertEqual(info['abis'], ['arm64-v8a', 'x86_64']) self.assertEqual(info['icon_png'], b'hi') # largest-density PNG, skipping the XML icon def test_missing_label_falls_back_to_package(self): info = self.read(apk({'AndroidManifest.xml': manifest('com.example.bare', 0x7f010000, 0x7f010001, 21)})) self.assertEqual(info['label'], 'com.example.bare') self.assertEqual(info['version'], '') self.assertEqual(info['abis'], []) def test_repacked_manifest(self): # Package kept only as a typed value; a foreign `label` mustn't beat android:label. arsc = resources({(1, '', 0): {0: 1, 1: 2}}) info = self.read(apk({ 'AndroidManifest.xml': manifest('com.example.repacked', 0x7f010000, 0x7f010001, 24, package_raw=False, foreign_label=True), 'resources.arsc': arsc, })) self.assertEqual(info['package'], 'com.example.repacked') self.assertEqual(info['label'], 'App label') self.assertIsNone(info['icon_png']) def test_rejects_non_apks(self): for data in (b'not a zip', apk({'classes.dex': b''}), apk({'AndroidManifest.xml': b''})): with self.assertRaises(frame_apk.ApkError): self.read(data) def test_refuses_oversized_members(self): # An APK from a website mustn't make the server inflate gigabytes. data = apk({'AndroidManifest.xml': manifest('com.example.big', 0x7f010000, 0x7f010001, 21)}) limit, frame_apk.MAX_MANIFEST = frame_apk.MAX_MANIFEST, 16 try: with self.assertRaises(frame_apk.ApkError): self.read(data) finally: frame_apk.MAX_MANIFEST = limit def test_forged_sizes_dont_inflate_everything(self): # The central directory claims 1 byte; the deflated data holds 16 MB of zeros. buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as z: z.writestr('AndroidManifest.xml', bytes(16 * 1024**2)) data = bytearray(buf.getvalue()) for sig, field in ((b'PK\x01\x02', 24), (b'PK\x03\x04', 22)): at = data.index(sig) data[at + field:at + field + 4] = struct.pack('