|  | # | 
|  | # Copyright (C) 2022 The Android Open Source Project | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | """ This script generates jarjar rule files to add a jarjar prefix to all classes, except those | 
|  | that are API, unsupported API or otherwise excluded.""" | 
|  |  | 
|  | import argparse | 
|  | import io | 
|  | import re | 
|  | import subprocess | 
|  | from xml import sax | 
|  | from xml.sax.handler import ContentHandler | 
|  | from zipfile import ZipFile | 
|  |  | 
|  |  | 
|  | def parse_arguments(argv): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument( | 
|  | 'jars', nargs='+', | 
|  | help='Path to pre-jarjar JAR. Multiple jars can be specified.') | 
|  | parser.add_argument( | 
|  | '--prefix', required=True, | 
|  | help='Package prefix to use for jarjared classes, ' | 
|  | 'for example "com.android.connectivity" (does not end with a dot).') | 
|  | parser.add_argument( | 
|  | '--output', required=True, help='Path to output jarjar rules file.') | 
|  | parser.add_argument( | 
|  | '--apistubs', action='append', default=[], | 
|  | help='Path to API stubs jar. Classes that are API will not be jarjared. Can be repeated to ' | 
|  | 'specify multiple jars.') | 
|  | parser.add_argument( | 
|  | '--unsupportedapi', | 
|  | help='Column(:)-separated paths to UnsupportedAppUsage hidden API .txt lists. ' | 
|  | 'Classes that have UnsupportedAppUsage API will not be jarjared.') | 
|  | parser.add_argument( | 
|  | '--excludes', action='append', default=[], | 
|  | help='Path to files listing classes that should not be jarjared. Can be repeated to ' | 
|  | 'specify multiple files.' | 
|  | 'Each file should contain one full-match regex per line. Empty lines or lines ' | 
|  | 'starting with "#" are ignored.') | 
|  | return parser.parse_args(argv) | 
|  |  | 
|  |  | 
|  | def _list_toplevel_jar_classes(jar): | 
|  | """List all classes in a .class .jar file that are not inner classes.""" | 
|  | return {_get_toplevel_class(c) for c in _list_jar_classes(jar)} | 
|  |  | 
|  | def _list_jar_classes(jar): | 
|  | with ZipFile(jar, 'r') as zip: | 
|  | files = zip.namelist() | 
|  | assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \ | 
|  | 'expected an intermediate zip of .class files' | 
|  | class_len = len('.class') | 
|  | return [f.replace('/', '.')[:-class_len] for f in files | 
|  | if f.endswith('.class') and not f.endswith('/package-info.class')] | 
|  |  | 
|  |  | 
|  | def _list_hiddenapi_classes(txt_file): | 
|  | out = set() | 
|  | with open(txt_file, 'r') as f: | 
|  | for line in f: | 
|  | if not line.strip(): | 
|  | continue | 
|  | assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}' | 
|  | clazz = line.replace('/', '.').split(';')[0][1:] | 
|  | out.add(_get_toplevel_class(clazz)) | 
|  | return out | 
|  |  | 
|  |  | 
|  | def _get_toplevel_class(clazz): | 
|  | """Return the name of the toplevel (not an inner class) enclosing class of the given class.""" | 
|  | if '$' not in clazz: | 
|  | return clazz | 
|  | return clazz.split('$')[0] | 
|  |  | 
|  |  | 
|  | def _get_excludes(path): | 
|  | out = [] | 
|  | with open(path, 'r') as f: | 
|  | for line in f: | 
|  | stripped = line.strip() | 
|  | if not stripped or stripped.startswith('#'): | 
|  | continue | 
|  | out.append(re.compile(stripped)) | 
|  | return out | 
|  |  | 
|  |  | 
|  | def make_jarjar_rules(args): | 
|  | excluded_classes = set() | 
|  | for apistubs_file in args.apistubs: | 
|  | excluded_classes.update(_list_toplevel_jar_classes(apistubs_file)) | 
|  |  | 
|  | unsupportedapi_files = (args.unsupportedapi and args.unsupportedapi.split(':')) or [] | 
|  | for unsupportedapi_file in unsupportedapi_files: | 
|  | if unsupportedapi_file: | 
|  | excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file)) | 
|  |  | 
|  | exclude_regexes = [] | 
|  | for exclude_file in args.excludes: | 
|  | exclude_regexes.extend(_get_excludes(exclude_file)) | 
|  |  | 
|  | with open(args.output, 'w') as outfile: | 
|  | for jar in args.jars: | 
|  | jar_classes = _list_jar_classes(jar) | 
|  | jar_classes.sort() | 
|  | for clazz in jar_classes: | 
|  | if (not clazz.startswith(args.prefix + '.') and | 
|  | _get_toplevel_class(clazz) not in excluded_classes and | 
|  | not any(r.fullmatch(clazz) for r in exclude_regexes)): | 
|  | outfile.write(f'rule {clazz} {args.prefix}.@0\n') | 
|  | # Also include jarjar rules for unit tests of the class, so the package matches | 
|  | outfile.write(f'rule {clazz}Test {args.prefix}.@0\n') | 
|  | outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n') | 
|  |  | 
|  |  | 
|  | def _main(): | 
|  | # Pass in None to use argv | 
|  | args = parse_arguments(None) | 
|  | make_jarjar_rules(args) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | _main() |