|  | #!/usr/bin/env python3 | 
|  | # Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file | 
|  | # for details. All rights reserved. Use of this source code is governed by a | 
|  | # BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | 
|  |  | 
|  | import apk_masseur | 
|  | import apk_utils | 
|  | import extractmarker | 
|  | import toolhelper | 
|  | import utils | 
|  | import zip_utils | 
|  |  | 
|  | def parse_options(argv): | 
|  | result = argparse.ArgumentParser( | 
|  | description='Instrument the dex files of a given apk to print what is ' | 
|  | 'executed.') | 
|  | result.add_argument('--apk', | 
|  | help='Path to the .apk', | 
|  | required=True) | 
|  | result.add_argument('--dex-files', | 
|  | action='append', | 
|  | help='Name of dex files to instrument') | 
|  | result.add_argument('--discard', | 
|  | action='append', | 
|  | help='Name of dex files to discard') | 
|  | result.add_argument('--out', | 
|  | help='Destination of resulting apk', | 
|  | required=True) | 
|  | options, args = result.parse_known_args(argv) | 
|  | return options, args | 
|  |  | 
|  | def add_instrumented_dex(dex_file, instrumented_dex_index, instrumented_dir): | 
|  | dex_name = get_dex_name(instrumented_dex_index) | 
|  | destination = os.path.join(instrumented_dir, dex_name) | 
|  | shutil.move(dex_file, destination) | 
|  |  | 
|  | def get_dex_name(dex_index): | 
|  | assert dex_index > 0 | 
|  | return 'classes.dex' if dex_index == 1 else ('classes%s.dex' % dex_index) | 
|  |  | 
|  | def instrument_dex_file(dex_file, include_instrumentation_server, options, tmp_dir): | 
|  | d8_cmd = [ | 
|  | 'java', | 
|  | '-cp', utils.R8_JAR, | 
|  | '-Dcom.android.tools.r8.startup.instrumentation.instrument=1', | 
|  | '-Dcom.android.tools.r8.startup.instrumentation.instrumentationtag=R8'] | 
|  | if not include_instrumentation_server: | 
|  | # We avoid injecting the InstrumentationServer by specifying it should only | 
|  | # be added if foo.bar.Baz is in the program. | 
|  | d8_cmd.append( | 
|  | '-Dcom.android.tools.r8.startup.instrumentation.instrumentationserversyntheticcontext=foo.bar.Baz') | 
|  | d8_cmd.extend([ | 
|  | 'com.android.tools.r8.D8', | 
|  | '--min-api', str(apk_utils.get_min_api(options.apk)), | 
|  | '--output', tmp_dir, | 
|  | '--release', | 
|  | dex_file]) | 
|  | subprocess.check_call(d8_cmd) | 
|  | instrumented_dex_files = [] | 
|  | instrumented_dex_index = 1 | 
|  | while True: | 
|  | instrumented_dex_name = get_dex_name(instrumented_dex_index) | 
|  | instrumented_dex_file = os.path.join(tmp_dir, instrumented_dex_name) | 
|  | if not os.path.exists(instrumented_dex_file): | 
|  | break | 
|  | instrumented_dex_files.append(instrumented_dex_file) | 
|  | instrumented_dex_index = instrumented_dex_index + 1 | 
|  | assert len(instrumented_dex_files) > 0 | 
|  | return instrumented_dex_files | 
|  |  | 
|  | def should_discard_dex_file(dex_name, options): | 
|  | return options.discard is not None and dex_name in options.discard | 
|  |  | 
|  | def should_instrument_dex_file(dex_name, options): | 
|  | return options.dex_files is not None and dex_name in options.dex_files | 
|  |  | 
|  | def main(argv): | 
|  | options, args = parse_options(argv) | 
|  | with utils.TempDir() as tmp_dir: | 
|  | # Extract the dex files of the apk. | 
|  | uninstrumented_dir = os.path.join(tmp_dir, 'uninstrumented') | 
|  | os.mkdir(uninstrumented_dir) | 
|  |  | 
|  | dex_predicate = \ | 
|  | lambda name : name.startswith('classes') and name.endswith('.dex') | 
|  | zip_utils.extract_all_that_matches( | 
|  | options.apk, uninstrumented_dir, dex_predicate) | 
|  |  | 
|  | # Instrument each dex one by one. | 
|  | instrumented_dir = os.path.join(tmp_dir, 'instrumented') | 
|  | os.mkdir(instrumented_dir) | 
|  |  | 
|  | include_instrumentation_server = True | 
|  | instrumented_dex_index = 1 | 
|  | uninstrumented_dex_index = 1 | 
|  | while True: | 
|  | dex_name = get_dex_name(uninstrumented_dex_index) | 
|  | dex_file = os.path.join(uninstrumented_dir, dex_name) | 
|  | if not os.path.exists(dex_file): | 
|  | break | 
|  | if not should_discard_dex_file(dex_name, options): | 
|  | if should_instrument_dex_file(dex_name, options): | 
|  | with utils.TempDir() as tmp_instrumentation_dir: | 
|  | instrumented_dex_files = \ | 
|  | instrument_dex_file( | 
|  | dex_file, | 
|  | include_instrumentation_server, | 
|  | options, | 
|  | tmp_instrumentation_dir) | 
|  | for instrumented_dex_file in instrumented_dex_files: | 
|  | add_instrumented_dex( | 
|  | instrumented_dex_file, instrumented_dex_index, instrumented_dir) | 
|  | instrumented_dex_index = instrumented_dex_index + 1 | 
|  | include_instrumentation_server = False | 
|  | else: | 
|  | add_instrumented_dex(dex_file, instrumented_dex_index, instrumented_dir) | 
|  | instrumented_dex_index = instrumented_dex_index + 1 | 
|  | uninstrumented_dex_index = uninstrumented_dex_index + 1 | 
|  |  | 
|  | assert instrumented_dex_index > 1 | 
|  |  | 
|  | # Masseur APK. | 
|  | apk_masseur.masseur(options.apk, dex=instrumented_dir, out=options.out) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main(sys.argv[1:])) |