# 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 : '' type : '(Ljava/lang/String;)V' code - catches : 1 0x000f - 0x001e -> 0x0093 positions : 0x0001 line=310 0x0057 line=313 locals : #1 : (in La;) name : '' 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.(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 : '' type : '(Ljava/lang/String;)V' code - catches : 1 0x000f - 0x001e -> 0x0093 positions : 0x0001 line=310 0x0057 line=313 locals : #1 : (in La;) name : '' 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('', 311, 313)), 1) self.assertEquals(len(dex['a'].FindMethodsAtLine('', 309, 315)), 1) clinit = dex['a'].FindMethodsAtLine('', 311, 313)[0] self.assertEquals(clinit.name, '') 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( '', '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()