"""frame_apk against a small APK built here: binary manifest plus resource table.""" import io import os import struct import sys import tempfile 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_reference_cycles_and_fan_out_are_bounded(self): res = frame_apk.Resources(b'') ref = frame_apk.T_REF res.entries = {1: [('', 0, ref, 1)] * 5} # five references to itself self.assertEqual(res.values(1), []) # Five references at each of five hops: 3125 leaves without a budget. res.entries = {i: [('', 0, ref, i + 1)] * 5 for i in range(1, 6)} res.entries[6] = [('', 0, frame_apk.T_STRING, 0)] self.assertEqual(len(res.values(1)), frame_apk.MAX_VALUES) if __name__ == '__main__': unittest.main()