#!/usr/bin/env python3
#
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import print_function

import collections
import optparse
import os
import re
import sys

from pylib import constants
from pylib.constants import host_paths

# pylint: disable=wrong-import-order
# Uses symbol.py from third_party/android_platform, not python's.
with host_paths.SysPath(
    host_paths.ANDROID_PLATFORM_DEVELOPMENT_SCRIPTS_PATH,
    position=0):
  import symbol


_RE_ASAN = re.compile(
    r"""
    (?P<prefix>.*?)
    (?P<pos>\#\S*?)          # position of the call in stack.
                             # escape the char "#" due to the VERBOSE flag.
    \s+(\S*?)\s+
    \(                       # match the char "(".
        (?P<lib>.*?)         # library path.
        \+0[xX](?P<addr>.*?) # address of the symbol in hex.
                             # the prefix "0x" is skipped.
    \)                       # match the char ")".
    """, re.VERBOSE)

# This named tuple models a parsed Asan log line.
AsanParsedLine = collections.namedtuple('AsanParsedLine',
                                        'prefix,library,pos,rel_address')

# This named tuple models an Asan log line. 'raw' is the raw content
# while 'parsed' is None or an AsanParsedLine instance.
AsanLogLine = collections.namedtuple('AsanLogLine', 'raw,parsed')

def _ParseAsanLogLine(line):
  """Parse line into corresponding AsanParsedLine value, if any, or None."""
  m = re.match(_RE_ASAN, line)
  if not m:
    return None
  return AsanParsedLine(prefix=m.group('prefix'),
                        library=m.group('lib'),
                        pos=m.group('pos'),
                        rel_address='%08x' % int(m.group('addr'), 16))


def _FindASanLibraries():
  asan_lib_dir = os.path.join(host_paths.DIR_SOURCE_ROOT,
                              'third_party', 'llvm-build',
                              'Release+Asserts', 'lib')
  asan_libs = []
  for src_dir, _, files in os.walk(asan_lib_dir):
    asan_libs += [os.path.relpath(os.path.join(src_dir, f))
                  for f in files
                  if f.endswith('.so')]
  return asan_libs


def _TranslateLibPath(library, asan_libs):
  for asan_lib in asan_libs:
    if os.path.basename(library) == os.path.basename(asan_lib):
      return '/' + asan_lib
  # pylint: disable=no-member
  return symbol.TranslateLibPath(library)


def _PrintSymbolized(asan_input, arch):
  """Print symbolized logcat output for Asan symbols.

  Args:
    asan_input: list of input lines.
    arch: Target CPU architecture.
  """
  asan_libs = _FindASanLibraries()

  # Maps library -> [ AsanParsedLine... ]
  libraries = collections.defaultdict(list)

  asan_log_lines = []
  for line in asan_input:
    line = line.rstrip()
    parsed = _ParseAsanLogLine(line)
    if parsed:
      libraries[parsed.library].append(parsed)
    asan_log_lines.append(AsanLogLine(raw=line, parsed=parsed))

  # Maps library -> { address -> [(symbol, location, obj_sym_with_offset)...] }
  all_symbols = collections.defaultdict(dict)

  for library, items in libraries.items():
    libname = _TranslateLibPath(library, asan_libs)
    lib_relative_addrs = set([i.rel_address for i in items])
    # pylint: disable=no-member
    info_dict = symbol.SymbolInformationForSet(libname,
                                               lib_relative_addrs,
                                               True,
                                               cpu_arch=arch)
    if info_dict:
      all_symbols[library] = info_dict

  for log_line in asan_log_lines:
    m = log_line.parsed
    if (m and m.library in all_symbols and
        m.rel_address in all_symbols[m.library]):
      # NOTE: all_symbols[lib][address] is a never-emtpy list of tuples.
      # NOTE: The documentation for SymbolInformationForSet() indicates
      # that usually one wants to display the last list item, not the first.
      # The code below takes the first, is this the best choice here?
      s = all_symbols[m.library][m.rel_address][0]
      print('%s%s %s %s' % (m.prefix, m.pos, s[0], s[1]))
    else:
      print(log_line.raw)


def main():
  parser = optparse.OptionParser()
  parser.add_option('-l', '--logcat',
                    help='File containing adb logcat output with ASan stacks. '
                         'Use stdin if not specified.')
  parser.add_option('--output-directory',
                    help='Path to the root build directory.')
  parser.add_option('--arch', default='arm',
                    help='CPU architecture name')
  options, _ = parser.parse_args()

  if options.output_directory:
    constants.SetOutputDirectory(options.output_directory)
  # Do an up-front test that the output directory is known.
  constants.CheckOutputDirectory()

  if options.logcat:
    asan_input = open(options.logcat, 'r')
  else:
    asan_input = sys.stdin

  _PrintSymbolized(asan_input.readlines(), options.arch)


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