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