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