| # Copyright 2018 The Chromium Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Tests for convert_dex_profile. | |
| Can be run from build/android/: | |
| $ cd build/android | |
| $ python convert_dex_profile_tests.py | |
| """ | |
| import os | |
| import sys | |
| import tempfile | |
| import unittest | |
| import convert_dex_profile as cp | |
| sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'gyp')) | |
| from util import build_utils | |
| cp.logging.disable(cp.logging.CRITICAL) | |
| # There are two obfuscations used in the tests below, each with the same | |
| # unobfuscated profile. The first, corresponding to DEX_DUMP, PROGUARD_MAPPING, | |
| # and OBFUSCATED_PROFILE, has an ambiguous method a() which is mapped to both | |
| # getInstance and initialize. The second, corresponding to DEX_DUMP_2, | |
| # PROGUARD_MAPPING_2 and OBFUSCATED_PROFILE_2, removes the ambiguity. | |
| DEX_DUMP = """ | |
| Class descriptor : 'La;' | |
| Direct methods - | |
| #0 : (in La;) | |
| name : '<clinit>' | |
| type : '(Ljava/lang/String;)V' | |
| code - | |
| catches : 1 | |
| 0x000f - 0x001e | |
| <any> -> 0x0093 | |
| positions : | |
| 0x0001 line=310 | |
| 0x0057 line=313 | |
| locals : | |
| #1 : (in La;) | |
| name : '<init>' | |
| type : '()V' | |
| positions : | |
| locals : | |
| Virtual methods - | |
| #0 : (in La;) | |
| name : 'a' | |
| type : '(Ljava/lang/String;)I' | |
| positions : | |
| 0x0000 line=2 | |
| 0x0003 line=3 | |
| 0x001b line=8 | |
| locals : | |
| 0x0000 - 0x0021 reg=3 this La; | |
| #1 : (in La;) | |
| name : 'a' | |
| type : '(Ljava/lang/Object;)I' | |
| positions : | |
| 0x0000 line=8 | |
| 0x0003 line=9 | |
| locals : | |
| 0x0000 - 0x0021 reg=3 this La; | |
| #2 : (in La;) | |
| name : 'b' | |
| type : '()La;' | |
| positions : | |
| 0x0000 line=1 | |
| locals : | |
| """ | |
| # pylint: disable=line-too-long | |
| PROGUARD_MAPPING = \ | |
| """org.chromium.Original -> a: | |
| org.chromium.Original sDisplayAndroidManager -> e | |
| org.chromium.Original another() -> b | |
| 4:4:void inlined():237:237 -> a | |
| 4:4:org.chromium.Original getInstance():203 -> a | |
| 5:5:void org.chromium.Original$Subclass.<init>(org.chromium.Original,byte):130:130 -> a | |
| 5:5:void initialize():237 -> a | |
| 5:5:org.chromium.Original getInstance():203 -> a | |
| 6:6:void initialize():237:237 -> a | |
| 9:9:android.content.Context org.chromium.base.ContextUtils.getApplicationContext():49:49 -> a | |
| 9:9:android.content.Context getContext():219 -> a | |
| 9:9:void initialize():245 -> a | |
| 9:9:org.chromium.Original getInstance():203 -> a""" | |
| OBFUSCATED_PROFILE = \ | |
| """La; | |
| PLa;->b()La; | |
| SLa;->a(Ljava/lang/Object;)I | |
| HPLa;->a(Ljava/lang/String;)I""" | |
| DEX_DUMP_2 = """ | |
| Class descriptor : 'La;' | |
| Direct methods - | |
| #0 : (in La;) | |
| name : '<clinit>' | |
| type : '(Ljava/lang/String;)V' | |
| code - | |
| catches : 1 | |
| 0x000f - 0x001e | |
| <any> -> 0x0093 | |
| positions : | |
| 0x0001 line=310 | |
| 0x0057 line=313 | |
| locals : | |
| #1 : (in La;) | |
| name : '<init>' | |
| type : '()V' | |
| positions : | |
| locals : | |
| Virtual methods - | |
| #0 : (in La;) | |
| name : 'a' | |
| type : '(Ljava/lang/String;)I' | |
| positions : | |
| 0x0000 line=2 | |
| 0x0003 line=3 | |
| 0x001b line=8 | |
| locals : | |
| 0x0000 - 0x0021 reg=3 this La; | |
| #1 : (in La;) | |
| name : 'c' | |
| type : '(Ljava/lang/Object;)I' | |
| positions : | |
| 0x0000 line=8 | |
| 0x0003 line=9 | |
| locals : | |
| 0x0000 - 0x0021 reg=3 this La; | |
| #2 : (in La;) | |
| name : 'b' | |
| type : '()La;' | |
| positions : | |
| 0x0000 line=1 | |
| locals : | |
| """ | |
| # pylint: disable=line-too-long | |
| PROGUARD_MAPPING_2 = \ | |
| """org.chromium.Original -> a: | |
| org.chromium.Original sDisplayAndroidManager -> e | |
| org.chromium.Original another() -> b | |
| void initialize() -> c | |
| org.chromium.Original getInstance():203 -> a | |
| 4:4:void inlined():237:237 -> a""" | |
| OBFUSCATED_PROFILE_2 = \ | |
| """La; | |
| PLa;->b()La; | |
| HPSLa;->a()La; | |
| HPLa;->c()V""" | |
| UNOBFUSCATED_PROFILE = \ | |
| """Lorg/chromium/Original; | |
| PLorg/chromium/Original;->another()Lorg/chromium/Original; | |
| HPSLorg/chromium/Original;->getInstance()Lorg/chromium/Original; | |
| HPLorg/chromium/Original;->initialize()V""" | |
| class GenerateProfileTests(unittest.TestCase): | |
| def testProcessDex(self): | |
| dex = cp.ProcessDex(DEX_DUMP.splitlines()) | |
| self.assertIsNotNone(dex['a']) | |
| self.assertEquals(len(dex['a'].FindMethodsAtLine('<clinit>', 311, 313)), 1) | |
| self.assertEquals(len(dex['a'].FindMethodsAtLine('<clinit>', 309, 315)), 1) | |
| clinit = dex['a'].FindMethodsAtLine('<clinit>', 311, 313)[0] | |
| self.assertEquals(clinit.name, '<clinit>') | |
| self.assertEquals(clinit.return_type, 'V') | |
| self.assertEquals(clinit.param_types, 'Ljava/lang/String;') | |
| self.assertEquals(len(dex['a'].FindMethodsAtLine('a', 8, None)), 2) | |
| self.assertIsNone(dex['a'].FindMethodsAtLine('a', 100, None)) | |
| # pylint: disable=protected-access | |
| def testProcessProguardMapping(self): | |
| dex = cp.ProcessDex(DEX_DUMP.splitlines()) | |
| mapping, reverse = cp.ProcessProguardMapping( | |
| PROGUARD_MAPPING.splitlines(), dex) | |
| self.assertEquals('La;', reverse.GetClassMapping('Lorg/chromium/Original;')) | |
| getInstance = cp.Method( | |
| 'getInstance', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') | |
| initialize = cp.Method('initialize', 'Lorg/chromium/Original;', '', 'V') | |
| another = cp.Method( | |
| 'another', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') | |
| subclassInit = cp.Method( | |
| '<init>', 'Lorg/chromium/Original$Subclass;', | |
| 'Lorg/chromium/Original;B', 'V') | |
| mapped = mapping.GetMethodMapping( | |
| cp.Method('a', 'La;', 'Ljava/lang/String;', 'I')) | |
| self.assertEquals(len(mapped), 2) | |
| self.assertIn(getInstance, mapped) | |
| self.assertNotIn(subclassInit, mapped) | |
| self.assertNotIn( | |
| cp.Method('inlined', 'Lorg/chromium/Original;', '', 'V'), mapped) | |
| self.assertIn(initialize, mapped) | |
| mapped = mapping.GetMethodMapping( | |
| cp.Method('a', 'La;', 'Ljava/lang/Object;', 'I')) | |
| self.assertEquals(len(mapped), 1) | |
| self.assertIn(getInstance, mapped) | |
| mapped = mapping.GetMethodMapping(cp.Method('b', 'La;', '', 'La;')) | |
| self.assertEquals(len(mapped), 1) | |
| self.assertIn(another, mapped) | |
| for from_method, to_methods in mapping._method_mapping.iteritems(): | |
| for to_method in to_methods: | |
| self.assertIn(from_method, reverse.GetMethodMapping(to_method)) | |
| for from_class, to_class in mapping._class_mapping.iteritems(): | |
| self.assertEquals(from_class, reverse.GetClassMapping(to_class)) | |
| def testProcessProfile(self): | |
| dex = cp.ProcessDex(DEX_DUMP.splitlines()) | |
| mapping, _ = cp.ProcessProguardMapping(PROGUARD_MAPPING.splitlines(), dex) | |
| profile = cp.ProcessProfile(OBFUSCATED_PROFILE.splitlines(), mapping) | |
| getInstance = cp.Method( | |
| 'getInstance', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') | |
| initialize = cp.Method('initialize', 'Lorg/chromium/Original;', '', 'V') | |
| another = cp.Method( | |
| 'another', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') | |
| self.assertIn('Lorg/chromium/Original;', profile._classes) | |
| self.assertIn(getInstance, profile._methods) | |
| self.assertIn(initialize, profile._methods) | |
| self.assertIn(another, profile._methods) | |
| self.assertEquals(profile._methods[getInstance], set(['H', 'S', 'P'])) | |
| self.assertEquals(profile._methods[initialize], set(['H', 'P'])) | |
| self.assertEquals(profile._methods[another], set(['P'])) | |
| def testEndToEnd(self): | |
| dex = cp.ProcessDex(DEX_DUMP.splitlines()) | |
| mapping, _ = cp.ProcessProguardMapping(PROGUARD_MAPPING.splitlines(), dex) | |
| profile = cp.ProcessProfile(OBFUSCATED_PROFILE.splitlines(), mapping) | |
| with tempfile.NamedTemporaryFile() as temp: | |
| profile.WriteToFile(temp.name) | |
| with open(temp.name, 'r') as f: | |
| for a, b in zip(sorted(f), sorted(UNOBFUSCATED_PROFILE.splitlines())): | |
| self.assertEquals(a.strip(), b.strip()) | |
| def testObfuscateProfile(self): | |
| with build_utils.TempDir() as temp_dir: | |
| # The dex dump is used as the dexfile, by passing /bin/cat as the dexdump | |
| # program. | |
| dex_path = os.path.join(temp_dir, 'dexdump') | |
| with open(dex_path, 'w') as dex_file: | |
| dex_file.write(DEX_DUMP_2) | |
| mapping_path = os.path.join(temp_dir, 'mapping') | |
| with open(mapping_path, 'w') as mapping_file: | |
| mapping_file.write(PROGUARD_MAPPING_2) | |
| unobfuscated_path = os.path.join(temp_dir, 'unobfuscated') | |
| with open(unobfuscated_path, 'w') as unobfuscated_file: | |
| unobfuscated_file.write(UNOBFUSCATED_PROFILE) | |
| obfuscated_path = os.path.join(temp_dir, 'obfuscated') | |
| cp.ObfuscateProfile(unobfuscated_path, dex_path, mapping_path, '/bin/cat', | |
| obfuscated_path) | |
| with open(obfuscated_path) as obfuscated_file: | |
| obfuscated_profile = sorted(obfuscated_file.readlines()) | |
| for a, b in zip( | |
| sorted(OBFUSCATED_PROFILE_2.splitlines()), obfuscated_profile): | |
| self.assertEquals(a.strip(), b.strip()) | |
| if __name__ == '__main__': | |
| unittest.main() |