1#!/usr/bin/env python3 2# Copyright 2018 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Tests for convert_dex_profile. 7 8Can be run from build/android/: 9 $ cd build/android 10 $ python convert_dex_profile_tests.py 11""" 12 13import os 14import sys 15import tempfile 16import unittest 17 18import convert_dex_profile as cp 19 20sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'gyp')) 21from util import build_utils 22 23cp.logging.disable(cp.logging.CRITICAL) 24 25# There are two obfuscations used in the tests below, each with the same 26# unobfuscated profile. The first, corresponding to DEX_DUMP, PROGUARD_MAPPING, 27# and OBFUSCATED_PROFILE, has an ambiguous method a() which is mapped to both 28# getInstance and initialize. The second, corresponding to DEX_DUMP_2, 29# PROGUARD_MAPPING_2 and OBFUSCATED_PROFILE_2, removes the ambiguity. 30 31DEX_DUMP = """ 32 33Class descriptor : 'La;' 34 Direct methods - 35 #0 : (in La;) 36 name : '<clinit>' 37 type : '(Ljava/lang/String;)V' 38 code - 39 catches : 1 40 0x000f - 0x001e 41 <any> -> 0x0093 42 positions : 43 0x0001 line=310 44 0x0057 line=313 45 locals : 46 #1 : (in La;) 47 name : '<init>' 48 type : '()V' 49 positions : 50 locals : 51 Virtual methods - 52 #0 : (in La;) 53 name : 'a' 54 type : '(Ljava/lang/String;)I' 55 positions : 56 0x0000 line=2 57 0x0003 line=3 58 0x001b line=8 59 locals : 60 0x0000 - 0x0021 reg=3 this La; 61 #1 : (in La;) 62 name : 'a' 63 type : '(Ljava/lang/Object;)I' 64 positions : 65 0x0000 line=8 66 0x0003 line=9 67 locals : 68 0x0000 - 0x0021 reg=3 this La; 69 #2 : (in La;) 70 name : 'b' 71 type : '()La;' 72 positions : 73 0x0000 line=1 74 locals : 75""" 76 77# pylint: disable=line-too-long 78PROGUARD_MAPPING = \ 79"""org.chromium.Original -> a: 80 org.chromium.Original sDisplayAndroidManager -> e 81 org.chromium.Original another() -> b 82 4:4:void inlined():237:237 -> a 83 4:4:org.chromium.Original getInstance():203 -> a 84 5:5:void org.chromium.Original$Subclass.<init>(org.chromium.Original,byte):130:130 -> a 85 5:5:void initialize():237 -> a 86 5:5:org.chromium.Original getInstance():203 -> a 87 6:6:void initialize():237:237 -> a 88 9:9:android.content.Context org.chromium.base.ContextUtils.getApplicationContext():49:49 -> a 89 9:9:android.content.Context getContext():219 -> a 90 9:9:void initialize():245 -> a 91 9:9:org.chromium.Original getInstance():203 -> a""" 92 93OBFUSCATED_PROFILE = \ 94"""La; 95PLa;->b()La; 96SLa;->a(Ljava/lang/Object;)I 97HPLa;->a(Ljava/lang/String;)I""" 98 99DEX_DUMP_2 = """ 100 101Class descriptor : 'La;' 102 Direct methods - 103 #0 : (in La;) 104 name : '<clinit>' 105 type : '(Ljava/lang/String;)V' 106 code - 107 catches : 1 108 0x000f - 0x001e 109 <any> -> 0x0093 110 positions : 111 0x0001 line=310 112 0x0057 line=313 113 locals : 114 #1 : (in La;) 115 name : '<init>' 116 type : '()V' 117 positions : 118 locals : 119 Virtual methods - 120 #0 : (in La;) 121 name : 'a' 122 type : '(Ljava/lang/String;)I' 123 positions : 124 0x0000 line=2 125 0x0003 line=3 126 0x001b line=8 127 locals : 128 0x0000 - 0x0021 reg=3 this La; 129 #1 : (in La;) 130 name : 'c' 131 type : '(Ljava/lang/Object;)I' 132 positions : 133 0x0000 line=8 134 0x0003 line=9 135 locals : 136 0x0000 - 0x0021 reg=3 this La; 137 #2 : (in La;) 138 name : 'b' 139 type : '()La;' 140 positions : 141 0x0000 line=1 142 locals : 143""" 144 145# pylint: disable=line-too-long 146PROGUARD_MAPPING_2 = \ 147"""org.chromium.Original -> a: 148 org.chromium.Original sDisplayAndroidManager -> e 149 org.chromium.Original another() -> b 150 void initialize() -> c 151 org.chromium.Original getInstance():203 -> a 152 4:4:void inlined():237:237 -> a""" 153 154OBFUSCATED_PROFILE_2 = \ 155"""La; 156PLa;->b()La; 157HPSLa;->a()La; 158HPLa;->c()V""" 159 160UNOBFUSCATED_PROFILE = \ 161"""Lorg/chromium/Original; 162PLorg/chromium/Original;->another()Lorg/chromium/Original; 163HPSLorg/chromium/Original;->getInstance()Lorg/chromium/Original; 164HPLorg/chromium/Original;->initialize()V""" 165 166class GenerateProfileTests(unittest.TestCase): 167 def testProcessDex(self): 168 dex = cp.ProcessDex(DEX_DUMP.splitlines()) 169 self.assertIsNotNone(dex['a']) 170 171 self.assertEqual(len(dex['a'].FindMethodsAtLine('<clinit>', 311, 313)), 1) 172 self.assertEqual(len(dex['a'].FindMethodsAtLine('<clinit>', 309, 315)), 1) 173 clinit = dex['a'].FindMethodsAtLine('<clinit>', 311, 313)[0] 174 self.assertEqual(clinit.name, '<clinit>') 175 self.assertEqual(clinit.return_type, 'V') 176 self.assertEqual(clinit.param_types, 'Ljava/lang/String;') 177 178 self.assertEqual(len(dex['a'].FindMethodsAtLine('a', 8, None)), 2) 179 self.assertIsNone(dex['a'].FindMethodsAtLine('a', 100, None)) 180 181# pylint: disable=protected-access 182 def testProcessProguardMapping(self): 183 dex = cp.ProcessDex(DEX_DUMP.splitlines()) 184 mapping, reverse = cp.ProcessProguardMapping( 185 PROGUARD_MAPPING.splitlines(), dex) 186 187 self.assertEqual('La;', reverse.GetClassMapping('Lorg/chromium/Original;')) 188 189 getInstance = cp.Method( 190 'getInstance', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') 191 initialize = cp.Method('initialize', 'Lorg/chromium/Original;', '', 'V') 192 another = cp.Method( 193 'another', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') 194 subclassInit = cp.Method( 195 '<init>', 'Lorg/chromium/Original$Subclass;', 196 'Lorg/chromium/Original;B', 'V') 197 198 mapped = mapping.GetMethodMapping( 199 cp.Method('a', 'La;', 'Ljava/lang/String;', 'I')) 200 self.assertEqual(len(mapped), 2) 201 self.assertIn(getInstance, mapped) 202 self.assertNotIn(subclassInit, mapped) 203 self.assertNotIn( 204 cp.Method('inlined', 'Lorg/chromium/Original;', '', 'V'), mapped) 205 self.assertIn(initialize, mapped) 206 207 mapped = mapping.GetMethodMapping( 208 cp.Method('a', 'La;', 'Ljava/lang/Object;', 'I')) 209 self.assertEqual(len(mapped), 1) 210 self.assertIn(getInstance, mapped) 211 212 mapped = mapping.GetMethodMapping(cp.Method('b', 'La;', '', 'La;')) 213 self.assertEqual(len(mapped), 1) 214 self.assertIn(another, mapped) 215 216 for from_method, to_methods in mapping._method_mapping.items(): 217 for to_method in to_methods: 218 self.assertIn(from_method, reverse.GetMethodMapping(to_method)) 219 for from_class, to_class in mapping._class_mapping.items(): 220 self.assertEqual(from_class, reverse.GetClassMapping(to_class)) 221 222 def testProcessProfile(self): 223 dex = cp.ProcessDex(DEX_DUMP.splitlines()) 224 mapping, _ = cp.ProcessProguardMapping(PROGUARD_MAPPING.splitlines(), dex) 225 profile = cp.ProcessProfile(OBFUSCATED_PROFILE.splitlines(), mapping) 226 227 getInstance = cp.Method( 228 'getInstance', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') 229 initialize = cp.Method('initialize', 'Lorg/chromium/Original;', '', 'V') 230 another = cp.Method( 231 'another', 'Lorg/chromium/Original;', '', 'Lorg/chromium/Original;') 232 233 self.assertIn('Lorg/chromium/Original;', profile._classes) 234 self.assertIn(getInstance, profile._methods) 235 self.assertIn(initialize, profile._methods) 236 self.assertIn(another, profile._methods) 237 238 self.assertEqual(profile._methods[getInstance], set(['H', 'S', 'P'])) 239 self.assertEqual(profile._methods[initialize], set(['H', 'P'])) 240 self.assertEqual(profile._methods[another], set(['P'])) 241 242 def testEndToEnd(self): 243 dex = cp.ProcessDex(DEX_DUMP.splitlines()) 244 mapping, _ = cp.ProcessProguardMapping(PROGUARD_MAPPING.splitlines(), dex) 245 246 profile = cp.ProcessProfile(OBFUSCATED_PROFILE.splitlines(), mapping) 247 with tempfile.NamedTemporaryFile() as temp: 248 profile.WriteToFile(temp.name) 249 with open(temp.name, 'r') as f: 250 for a, b in zip(sorted(f), sorted(UNOBFUSCATED_PROFILE.splitlines())): 251 self.assertEqual(a.strip(), b.strip()) 252 253 def testObfuscateProfile(self): 254 with build_utils.TempDir() as temp_dir: 255 # The dex dump is used as the dexfile, by passing /bin/cat as the dexdump 256 # program. 257 dex_path = os.path.join(temp_dir, 'dexdump') 258 with open(dex_path, 'w') as dex_file: 259 dex_file.write(DEX_DUMP_2) 260 mapping_path = os.path.join(temp_dir, 'mapping') 261 with open(mapping_path, 'w') as mapping_file: 262 mapping_file.write(PROGUARD_MAPPING_2) 263 unobfuscated_path = os.path.join(temp_dir, 'unobfuscated') 264 with open(unobfuscated_path, 'w') as unobfuscated_file: 265 unobfuscated_file.write(UNOBFUSCATED_PROFILE) 266 obfuscated_path = os.path.join(temp_dir, 'obfuscated') 267 cp.ObfuscateProfile(unobfuscated_path, dex_path, mapping_path, '/bin/cat', 268 obfuscated_path) 269 with open(obfuscated_path) as obfuscated_file: 270 obfuscated_profile = sorted(obfuscated_file.readlines()) 271 for a, b in zip( 272 sorted(OBFUSCATED_PROFILE_2.splitlines()), obfuscated_profile): 273 self.assertEqual(a.strip(), b.strip()) 274 275 276if __name__ == '__main__': 277 unittest.main() 278