#!/usr/bin/env python3
# Copyright 2012 Google LLC
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google LLC nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Normalizes and de-duplicates paths within Breakpad symbol files.

When using DWARF for storing debug symbols, some file information will be
stored relative to the current working directory of the current compilation
unit, and may be further relativized based upon how the file was #included.

This helper can be used to parse the Breakpad symbol file generated from such
DWARF files and normalize and de-duplicate the FILE records found within,
updating any references to the FILE records in the other record types.
"""

import ntpath
import optparse
import os
import posixpath
import sys

class BreakpadParseError(Exception):
  """Unsupported Breakpad symbol record exception class."""
  pass

class SymbolFileParser(object):
  """Parser for Breakpad symbol files.

  The format of these files is documented at
  https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md
  """

  def __init__(self, input_stream, output_stream, ignored_prefixes=None,
               path_handler=os.path):
    """Inits a SymbolFileParser to read symbol records from |input_stream| and
    write the processed output to |output_stream|.
    
    |ignored_prefixes| contains a list of optional path prefixes that
    should be stripped from the final, normalized path outputs.
    
    For example, if the Breakpad symbol file had all paths starting with a
    common prefix, such as:
      FILE 1 /b/build/src/foo.cc
      FILE 2 /b/build/src/bar.cc
    Then adding "/b/build/src" as an ignored prefix would result in an output
    file that contained:
      FILE 1 foo.cc
      FILE 2 bar.cc
    
    Note that |ignored_prefixes| does not necessarily contain file system
    paths, as the contents of the DWARF DW_AT_comp_dir attribute is dependent
    upon the host system and compiler, and may contain additional information
    such as hostname or compiler version.
    """

    self.unique_files = {}
    self.duplicate_files = {}
    self.input_stream = input_stream
    self.output_stream = output_stream
    self.ignored_prefixes = ignored_prefixes or []
    self.path_handler = path_handler

  def Process(self):
    """Processes the Breakpad symbol file."""
    for line in self.input_stream:
      parsed = self._ParseRecord(line.rstrip())
      if parsed:
        self.output_stream.write(parsed + '\n')

  def _ParseRecord(self, record):
    """Parses a single Breakpad symbol record - a single line from the symbol
    file.

    Returns:
        The modified string to write to the output file, or None if no line
        should be written.
    """
    record_type = record.partition(' ')[0]
    if record_type == 'FILE':
      return self._ParseFileRecord(record)
    elif self._IsLineRecord(record_type):
      return self._ParseLineRecord(record)
    else:
      # Simply pass the record through unaltered.
      return record

  def _NormalizePath(self, path):
    """Normalizes a file path to its canonical form.

    As this may not execute on the machine or file system originally
    responsible for compilation, it may be necessary to further correct paths
    for symlinks, junctions, or other such file system indirections.

    Returns:
        A unique, canonical representation for the the file path.
    """
    return self.path_handler.normpath(path)

  def _AdjustPath(self, path):
    """Adjusts the supplied path after performing path de-duplication.

    This may be used to perform secondary adjustments, such as removing a
    common prefix, such as "/D/build", or replacing the file system path with
    information from the version control system.

    Returns:
        The actual path to use when writing the FILE record.
    """
    return path[len(next(filter(path.startswith,
                                self.ignored_prefixes + ['']))):]

  def _ParseFileRecord(self, file_record):
    """Parses and corrects a FILE record."""
    file_info = file_record[5:].split(' ', 3)
    if len(file_info) > 2:
      raise BreakpadParseError('Unsupported FILE record: ' + file_record)
    file_index = int(file_info[0])
    file_name = self._NormalizePath(file_info[1])
    existing_file_index = self.unique_files.get(file_name)
    if existing_file_index is None:
      self.unique_files[file_name] = file_index
      file_info[1] = self._AdjustPath(file_name)
      return 'FILE ' + ' '.join(file_info)
    else:
      self.duplicate_files[file_index] = existing_file_index
      return None

  def _IsLineRecord(self, record_type):
    """Determines if the current record type is a Line record"""
    try:
      line = int(record_type, 16)
    except (ValueError, TypeError):
      return False
    return True

  def _ParseLineRecord(self, line_record):
    """Parses and corrects a Line record."""
    line_info = line_record.split(' ', 5)
    if len(line_info) > 4:
      raise BreakpadParseError('Unsupported Line record: ' + line_record)
    file_index = int(line_info[3])
    line_info[3] = str(self.duplicate_files.get(file_index, file_index))
    return ' '.join(line_info)

def main():
  option_parser = optparse.OptionParser()
  option_parser.add_option("-p", "--prefix",
                           action="append", dest="prefixes", type="string",
                           default=[],
                           help="A path prefix that should be removed from "
                                "all FILE lines. May be repeated to specify "
                                "multiple prefixes.")
  option_parser.add_option("-t", "--path_type",
                           action="store", type="choice", dest="path_handler",
                           choices=['win32', 'posix'],
                           help="Indicates how file paths should be "
                                "interpreted. The default is to treat paths "
                                "the same as the OS running Python (eg: "
                                "os.path)")
  options, args = option_parser.parse_args()
  if args:
    option_parser.error('Unknown argument: %s' % args)

  path_handler = { 'win32': ntpath,
                   'posix': posixpath }.get(options.path_handler, os.path)
  try:
    symbol_parser = SymbolFileParser(sys.stdin, sys.stdout, options.prefixes,
                                     path_handler)
    symbol_parser.Process()
  except BreakpadParseError as e:
    print >> sys.stderr, 'Got an error while processing symbol file'
    print >> sys.stderr, str(e)
    return 1
  return 0

if __name__ == '__main__':
  sys.exit(main())
