1#
2# Copyright (C) 2022 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16""" This script generates jarjar rule files to add a jarjar prefix to all classes, except those
17that are API, unsupported API or otherwise excluded."""
18
19import argparse
20import io
21import re
22import subprocess
23from xml import sax
24from xml.sax.handler import ContentHandler
25from zipfile import ZipFile
26
27
28def parse_arguments(argv):
29    parser = argparse.ArgumentParser()
30    parser.add_argument(
31        'jars', nargs='+',
32        help='Path to pre-jarjar JAR. Multiple jars can be specified.')
33    parser.add_argument(
34        '--prefix', required=True,
35        help='Package prefix to use for jarjared classes, '
36             'for example "com.android.connectivity" (does not end with a dot).')
37    parser.add_argument(
38        '--output', required=True, help='Path to output jarjar rules file.')
39    parser.add_argument(
40        '--apistubs', action='append', default=[],
41        help='Path to API stubs jar. Classes that are API will not be jarjared. Can be repeated to '
42             'specify multiple jars.')
43    parser.add_argument(
44        '--unsupportedapi',
45        help='Column(:)-separated paths to UnsupportedAppUsage hidden API .txt lists. '
46             'Classes that have UnsupportedAppUsage API will not be jarjared.')
47    parser.add_argument(
48        '--excludes', action='append', default=[],
49        help='Path to files listing classes that should not be jarjared. Can be repeated to '
50             'specify multiple files.'
51             'Each file should contain one full-match regex per line. Empty lines or lines '
52             'starting with "#" are ignored.')
53    return parser.parse_args(argv)
54
55
56def _list_toplevel_jar_classes(jar):
57    """List all classes in a .class .jar file that are not inner classes."""
58    return {_get_toplevel_class(c) for c in _list_jar_classes(jar)}
59
60def _list_jar_classes(jar):
61    with ZipFile(jar, 'r') as zip:
62        files = zip.namelist()
63        assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
64                                           'expected an intermediate zip of .class files'
65        class_len = len('.class')
66        return [f.replace('/', '.')[:-class_len] for f in files
67                if f.endswith('.class') and not f.endswith('/package-info.class')]
68
69
70def _list_hiddenapi_classes(txt_file):
71    out = set()
72    with open(txt_file, 'r') as f:
73        for line in f:
74            if not line.strip():
75                continue
76            assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
77            clazz = line.replace('/', '.').split(';')[0][1:]
78            out.add(_get_toplevel_class(clazz))
79    return out
80
81
82def _get_toplevel_class(clazz):
83    """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
84    if '$' not in clazz:
85        return clazz
86    return clazz.split('$')[0]
87
88
89def _get_excludes(path):
90    out = []
91    with open(path, 'r') as f:
92        for line in f:
93            stripped = line.strip()
94            if not stripped or stripped.startswith('#'):
95                continue
96            out.append(re.compile(stripped))
97    return out
98
99
100def make_jarjar_rules(args):
101    excluded_classes = set()
102    for apistubs_file in args.apistubs:
103        excluded_classes.update(_list_toplevel_jar_classes(apistubs_file))
104
105    unsupportedapi_files = (args.unsupportedapi and args.unsupportedapi.split(':')) or []
106    for unsupportedapi_file in unsupportedapi_files:
107        if unsupportedapi_file:
108            excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
109
110    exclude_regexes = []
111    for exclude_file in args.excludes:
112        exclude_regexes.extend(_get_excludes(exclude_file))
113
114    with open(args.output, 'w') as outfile:
115        for jar in args.jars:
116            jar_classes = _list_jar_classes(jar)
117            jar_classes.sort()
118            for clazz in jar_classes:
119                if (not clazz.startswith(args.prefix + '.') and
120                        _get_toplevel_class(clazz) not in excluded_classes and
121                        not any(r.fullmatch(clazz) for r in exclude_regexes)):
122                    outfile.write(f'rule {clazz} {args.prefix}.@0\n')
123                    # Also include jarjar rules for unit tests of the class if it's not explicitly
124                    # excluded, so the package matches
125                    if not any(r.fullmatch(clazz + 'Test') for r in exclude_regexes):
126                        outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
127                        outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
128
129
130def _main():
131    # Pass in None to use argv
132    args = parse_arguments(None)
133    make_jarjar_rules(args)
134
135
136if __name__ == '__main__':
137    _main()
138