1#!/usr/bin/env python3
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import argparse
19import gzip
20import json
21import os
22import sys
23import zipfile
24
25
26class LsdumpError(Exception):
27    """The exception raised by ParseLsdumpFile."""
28    pass
29
30
31class AttrDict(dict):
32    """A dictionary with attribute accessors."""
33
34    def __getattr__(self, key):
35        """Returns self[key]."""
36        try:
37            return self[key]
38        except KeyError:
39            raise AttributeError('Failed to get attribute: %s' % key)
40
41    def __setattr__(self, key, value):
42        """Assigns value to self[key]."""
43        self[key] = value
44
45
46def _GetTypeSymbol(record_type):
47    """Gets the mangled name of a record type.
48
49    Before Android R, unique_id was mangled name starting with '_ZTS'.
50    linker_set_key was unmangled.
51    Since Android R, unique_id has been removed, and linker_set_key has
52    been changed to mangled name starting with '_ZTI'.
53
54    Args:
55        record_type: An AttrDict, a record type in lsdump.
56
57    Returns:
58        A string, the mangled name starting with '_ZTI'.
59    """
60    if 'unique_id' in record_type:
61        return record_type['unique_id'].replace('_ZTS', '_ZTI', 1)
62    return record_type['linker_set_key']
63
64
65def _OpenFileOrGzipped(file_name):
66    """Opens a file that is either in gzip or uncompressed format.
67
68    If file_name ends with '.gz' then an opened gzip text file, else return an
69    opened text file.
70
71    Args:
72        file_name: The file name to open.
73
74    Returns:
75        A file object.
76
77    Raises:
78        IOError if fails to open.
79    """
80    if file_name.endswith('.gz'):
81        return gzip.open(file_name, 'rt')
82    return open(file_name, 'r')
83
84
85def _ConsumeOffset(tok, beg=0):
86    """Consumes a <offset-number> in a thunk symbol."""
87    pos = tok.find('_', beg) + 1
88    return tok[:pos], tok[pos:]
89
90
91def _ConsumeCallOffset(tok):
92    """Consumes a <call-offset> in a thunk symbol."""
93    if tok[:1] == 'h':
94        lhs, rhs = _ConsumeOffset(tok, 1)
95    elif tok[:1] == 'v':
96        lhs, rhs = _ConsumeOffset(tok, 1)
97        lhs2, rhs = _ConsumeOffset(rhs)
98        if lhs and lhs2:
99            lhs = lhs + lhs2
100        else:
101            lhs, rhs = '', tok
102    else:
103        lhs, rhs = '', tok
104    return lhs, rhs
105
106
107def _FindThunkTarget(name):
108    """Finds thunk symbol's target function.
109
110    <thunk-symbol> ::= _ZT <call-offset> <base-encoding>
111                     | _ZTc <call-offset> <call-offset> <base-encoding>
112    <call-offset>  ::= h <nv-offset>
113                     | v <v-offset>
114    <nv-offset>    ::= <offset-number> _
115    <v-offset>     ::= <offset-number> _ <offset-number> _
116
117    Args:
118        name: A string, the symbol name to resolve.
119
120    Returns:
121        A string, symbol name of the nominal target function.
122        The input symbol name if it is not a thunk symbol.
123    """
124    if name.startswith('_ZTh') or name.startswith('_ZTv'):
125        lhs, rhs = _ConsumeCallOffset(name[len('_ZT'):])
126        if lhs:
127            return '_Z' + rhs
128    if name.startswith('_ZTc'):
129        lhs, rhs = _ConsumeCallOffset(name[len('_ZTc'):])
130        lhs2, rhs = _ConsumeCallOffset(rhs)
131        if lhs and lhs2:
132            return '_Z' + rhs
133    return name
134
135
136def _FilterElfFunctions(lsdump):
137    """Finds exported functions and thunks in lsdump.
138
139    Args:
140        lsdump: An AttrDict object containing the lsdump.
141
142    Yields:
143        The AttrDict objects in lsdump.elf_functions.
144    """
145    functions = {function.linker_set_key for function in lsdump.functions}
146
147    for elf_function in lsdump.elf_functions:
148        if elf_function.name in functions:
149            yield elf_function
150        elif _FindThunkTarget(elf_function.name) in functions:
151            yield elf_function
152
153
154def _FilterElfObjects(lsdump):
155    """Finds exported variables, type info, and vtables in lsdump.
156
157    Args:
158        lsdump: An AttrDict object containing the lsdump.
159
160    Yields:
161        The AttrDict objects in lsdump.elf_objects.
162    """
163    global_vars = {global_var.linker_set_key
164                   for global_var in lsdump.global_vars}
165    record_names = {_GetTypeSymbol(record_type)[len('_ZTI'):]
166                    for record_type in lsdump.record_types}
167
168    for elf_object in lsdump.elf_objects:
169        name = elf_object.name
170        if name in global_vars:
171            yield elf_object
172        elif (name[:len('_ZTI')] in {'_ZTV', '_ZTT', '_ZTI', '_ZTS'} and
173                name[len('_ZTI'):] in record_names):
174            yield elf_object
175
176
177def _ParseSymbolsFromLsdump(lsdump, output_dump):
178    """Parses symbols from an lsdump.
179
180    Args:
181        lsdump: An AttrDict object containing the lsdump.
182        output_dump: An AttrDict object containing the output.
183    """
184    output_dump.elf_functions = list(_FilterElfFunctions(lsdump))
185    output_dump.elf_objects = list(_FilterElfObjects(lsdump))
186
187
188def _ParseVtablesFromLsdump(lsdump, output_dump):
189    """Parses vtables from an lsdump.
190
191    Args:
192        lsdump: An AttrDict object containing the lsdump.
193        output_dump: An AttrDict object containing the output.
194    """
195    vtable_symbols = {elf_object.name for elf_object in lsdump.elf_objects
196                      if elf_object.name.startswith('_ZTV')}
197
198    output_dump.record_types = []
199    for lsdump_record_type in lsdump.record_types:
200        type_symbol = _GetTypeSymbol(lsdump_record_type)
201        vtable_symbol = '_ZTV' + type_symbol[len('_ZTI'):]
202        if vtable_symbol not in vtable_symbols:
203            continue
204        record_type = AttrDict()
205        record_type.linker_set_key = type_symbol
206        record_type.vtable_components = lsdump_record_type.vtable_components
207        output_dump.record_types.append(record_type)
208
209
210def _ParseLsdumpFileObject(lsdump_file):
211    """Converts an lsdump file to a dump object.
212
213    Args:
214        lsdump_file: The file object of the input lsdump.
215
216    Returns:
217        The AttrDict object converted from the lsdump file.
218
219    Raises:
220        LsdumpError if fails to create the dump file.
221    """
222    try:
223        lsdump = json.load(lsdump_file, object_hook=AttrDict)
224    except ValueError:
225        raise LsdumpError(e)
226
227    try:
228        output_dump = AttrDict()
229        _ParseVtablesFromLsdump(lsdump, output_dump)
230        _ParseSymbolsFromLsdump(lsdump, output_dump)
231        return output_dump
232    except AttributeError as e:
233        raise LsdumpError(e)
234
235
236def _ParseLsdumpZipFile(lsdump_zip, output_zip):
237    """Converts zipped lsdump files to the dump files for ABI test."""
238    for name in lsdump_zip.namelist():
239        if name.endswith('.lsdump'):
240            with lsdump_zip.open(name, mode='r') as lsdump_file:
241                output_dump = _ParseLsdumpFileObject(lsdump_file)
242            output_str = json.dumps(output_dump, indent=1,
243                                    separators=(',', ':'))
244            output_zip.writestr(name, output_str)
245
246
247def ParseLsdumpFile(input_path, output_path):
248    """Converts an lsdump file to a dump file for the ABI test.
249
250    Args:
251        input_path: The path to the (gzipped) lsdump file.
252        output_path: The path to the output dump file.
253
254    Raises:
255        LsdumpError if fails to create the dump file.
256    """
257    abs_output_path = os.path.abspath(output_path)
258    abs_output_dir = os.path.dirname(abs_output_path)
259
260    try:
261        if abs_output_dir and not os.path.exists(abs_output_dir):
262            os.makedirs(abs_output_dir)
263
264        with _OpenFileOrGzipped(input_path) as lsdump_file:
265            output_dump = _ParseLsdumpFileObject(lsdump_file)
266        with open(output_path, 'w') as output_file:
267            json.dump(output_dump, output_file,
268                      indent=1, separators=(',', ':'))
269    except (IOError, OSError) as e:
270        raise LsdumpError(e)
271
272
273def main():
274    arg_parser = argparse.ArgumentParser(
275        description='This script converts an lsdump file to a dump file for '
276                    'the ABI test in VTS.')
277    arg_parser.add_argument('input_path',
278                            help='input lsdump file path.')
279    arg_parser.add_argument('output_path',
280                            help='output dump file path.')
281    args = arg_parser.parse_args()
282
283    # TODO(b/150663999): Remove this when the build system is able to process
284    #                    the large file group.
285    if zipfile.is_zipfile(args.input_path):
286        with zipfile.ZipFile(args.input_path, mode='r') as input_zip:
287            # The zip will be added to a Python package. It is not necessary
288            # to reduce the file size.
289            with zipfile.ZipFile(args.output_path, mode='w',
290                                 compression=zipfile.ZIP_STORED) as output_zip:
291                _ParseLsdumpZipFile(input_zip, output_zip)
292        sys.exit(0)
293
294    try:
295        ParseLsdumpFile(args.input_path, args.output_path)
296    except LsdumpError as e:
297        print(e)
298        sys.exit(1)
299
300
301if __name__ == '__main__':
302    main()
303