/ src / tools / python / filter_syms.py
filter_syms.py
  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  
 32  When using DWARF for storing debug symbols, some file information will be
 33  stored relative to the current working directory of the current compilation
 34  unit, and may be further relativized based upon how the file was #included.
 35  
 36  This helper can be used to parse the Breakpad symbol file generated from such
 37  DWARF files and normalize and de-duplicate the FILE records found within,
 38  updating any references to the FILE records in the other record types.
 39  """
 40  
 41  import ntpath
 42  import optparse
 43  import os
 44  import posixpath
 45  import sys
 46  
 47  class BreakpadParseError(Exception):
 48    """Unsupported Breakpad symbol record exception class."""
 49    pass
 50  
 51  class 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  
170  def 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  
201  if __name__ == '__main__':
202    sys.exit(main())