xref: /aosp_15_r20/external/google-breakpad/src/tools/python/filter_syms.py (revision 9712c20fc9bbfbac4935993a2ca0b3958c5adad2)
1#!/usr/bin/env python3
2# Copyright 2012 Google LLC
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#     * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#     * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#     * Neither the name of Google LLC nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30"""Normalizes and de-duplicates paths within Breakpad symbol files.
31
32When using DWARF for storing debug symbols, some file information will be
33stored relative to the current working directory of the current compilation
34unit, and may be further relativized based upon how the file was #included.
35
36This helper can be used to parse the Breakpad symbol file generated from such
37DWARF files and normalize and de-duplicate the FILE records found within,
38updating any references to the FILE records in the other record types.
39"""
40
41import ntpath
42import optparse
43import os
44import posixpath
45import sys
46
47class BreakpadParseError(Exception):
48  """Unsupported Breakpad symbol record exception class."""
49  pass
50
51class SymbolFileParser(object):
52  """Parser for Breakpad symbol files.
53
54  The format of these files is documented at
55  https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md
56  """
57
58  def __init__(self, input_stream, output_stream, ignored_prefixes=None,
59               path_handler=os.path):
60    """Inits a SymbolFileParser to read symbol records from |input_stream| and
61    write the processed output to |output_stream|.
62
63    |ignored_prefixes| contains a list of optional path prefixes that
64    should be stripped from the final, normalized path outputs.
65
66    For example, if the Breakpad symbol file had all paths starting with a
67    common prefix, such as:
68      FILE 1 /b/build/src/foo.cc
69      FILE 2 /b/build/src/bar.cc
70    Then adding "/b/build/src" as an ignored prefix would result in an output
71    file that contained:
72      FILE 1 foo.cc
73      FILE 2 bar.cc
74
75    Note that |ignored_prefixes| does not necessarily contain file system
76    paths, as the contents of the DWARF DW_AT_comp_dir attribute is dependent
77    upon the host system and compiler, and may contain additional information
78    such as hostname or compiler version.
79    """
80
81    self.unique_files = {}
82    self.duplicate_files = {}
83    self.input_stream = input_stream
84    self.output_stream = output_stream
85    self.ignored_prefixes = ignored_prefixes or []
86    self.path_handler = path_handler
87
88  def Process(self):
89    """Processes the Breakpad symbol file."""
90    for line in self.input_stream:
91      parsed = self._ParseRecord(line.rstrip())
92      if parsed:
93        self.output_stream.write(parsed + '\n')
94
95  def _ParseRecord(self, record):
96    """Parses a single Breakpad symbol record - a single line from the symbol
97    file.
98
99    Returns:
100        The modified string to write to the output file, or None if no line
101        should be written.
102    """
103    record_type = record.partition(' ')[0]
104    if record_type == 'FILE':
105      return self._ParseFileRecord(record)
106    elif self._IsLineRecord(record_type):
107      return self._ParseLineRecord(record)
108    else:
109      # Simply pass the record through unaltered.
110      return record
111
112  def _NormalizePath(self, path):
113    """Normalizes a file path to its canonical form.
114
115    As this may not execute on the machine or file system originally
116    responsible for compilation, it may be necessary to further correct paths
117    for symlinks, junctions, or other such file system indirections.
118
119    Returns:
120        A unique, canonical representation for the the file path.
121    """
122    return self.path_handler.normpath(path)
123
124  def _AdjustPath(self, path):
125    """Adjusts the supplied path after performing path de-duplication.
126
127    This may be used to perform secondary adjustments, such as removing a
128    common prefix, such as "/D/build", or replacing the file system path with
129    information from the version control system.
130
131    Returns:
132        The actual path to use when writing the FILE record.
133    """
134    return path[len(next(filter(path.startswith,
135                                self.ignored_prefixes + ['']))):]
136
137  def _ParseFileRecord(self, file_record):
138    """Parses and corrects a FILE record."""
139    file_info = file_record[5:].split(' ', 3)
140    if len(file_info) > 2:
141      raise BreakpadParseError('Unsupported FILE record: ' + file_record)
142    file_index = int(file_info[0])
143    file_name = self._NormalizePath(file_info[1])
144    existing_file_index = self.unique_files.get(file_name)
145    if existing_file_index is None:
146      self.unique_files[file_name] = file_index
147      file_info[1] = self._AdjustPath(file_name)
148      return 'FILE ' + ' '.join(file_info)
149    else:
150      self.duplicate_files[file_index] = existing_file_index
151      return None
152
153  def _IsLineRecord(self, record_type):
154    """Determines if the current record type is a Line record"""
155    try:
156      line = int(record_type, 16)
157    except (ValueError, TypeError):
158      return False
159    return True
160
161  def _ParseLineRecord(self, line_record):
162    """Parses and corrects a Line record."""
163    line_info = line_record.split(' ', 5)
164    if len(line_info) > 4:
165      raise BreakpadParseError('Unsupported Line record: ' + line_record)
166    file_index = int(line_info[3])
167    line_info[3] = str(self.duplicate_files.get(file_index, file_index))
168    return ' '.join(line_info)
169
170def main():
171  option_parser = optparse.OptionParser()
172  option_parser.add_option("-p", "--prefix",
173                           action="append", dest="prefixes", type="string",
174                           default=[],
175                           help="A path prefix that should be removed from "
176                                "all FILE lines. May be repeated to specify "
177                                "multiple prefixes.")
178  option_parser.add_option("-t", "--path_type",
179                           action="store", type="choice", dest="path_handler",
180                           choices=['win32', 'posix'],
181                           help="Indicates how file paths should be "
182                                "interpreted. The default is to treat paths "
183                                "the same as the OS running Python (eg: "
184                                "os.path)")
185  options, args = option_parser.parse_args()
186  if args:
187    option_parser.error('Unknown argument: %s' % args)
188
189  path_handler = { 'win32': ntpath,
190                   'posix': posixpath }.get(options.path_handler, os.path)
191  try:
192    symbol_parser = SymbolFileParser(sys.stdin, sys.stdout, options.prefixes,
193                                     path_handler)
194    symbol_parser.Process()
195  except BreakpadParseError as e:
196    print >> sys.stderr, 'Got an error while processing symbol file'
197    print >> sys.stderr, str(e)
198    return 1
199  return 0
200
201if __name__ == '__main__':
202  sys.exit(main())
203