1#!/usr/bin/env python3
2# Copyright 2018 The Chromium Authors. All rights reserved.
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