#!/usr/bin/env vpython3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Check that symbols are ordered into a binary as they appear in the orderfile."""

import argparse
import dataclasses
import logging
import pathlib
import sys
from typing import Dict
from typing import Set

import orderfile_shared
import symbol_extractor

_DEFAULT_THRESHOLD = 80


@dataclasses.dataclass
class _OffsetGroup:
  """Represents a set of symbols sharing the same offset."""

  offset: int
  names: Set[str] = dataclasses.field(default_factory=set)
  used: bool = False


def _NamesToOffsetGroups(
  symbol_infos: symbol_extractor.SymbolInfo,
) -> Dict[str, _OffsetGroup]:
  """Groups all symbols by offset."""
  name_to_group = {}
  offset_to_group = {}
  for si in symbol_infos:
    if not si.offset in offset_to_group:
      group = _OffsetGroup(si.offset)
      offset_to_group[si.offset] = group
    group = offset_to_group[si.offset]
    group.names.add(si.name)
    name_to_group[si.name] = group
  return name_to_group


def _MultipleAddressSymbols(
  symbol_infos: symbol_extractor.SymbolInfo,
  name_to_group: Dict[str, _OffsetGroup],
):
  """Finds a set of symbols appearing at the same address."""
  ret = set()
  for si in symbol_infos:
    if si.name in name_to_group and si.offset != name_to_group[si.name].offset:
      ret.add(si.name)
  return ret


def _VerifySymbolOrder(orderfile_symbols, symbol_infos, threshold):
  """Verify symbol ordering.

  Checks that the non-section symbols in |orderfile_filename| are consistent
  with the offsets |symbol_infos|.

  Args:
    orderfile_symbols: ([str]) list of symbols from orderfile.
    symbol_infos: ([SymbolInfo]) symbol infos from binary.
    threshold: (int) The number of misordered symbols beyond which we error.

  Returns:
    True iff the ordering is consistent within |threshold|.
  """
  name_to_group = _NamesToOffsetGroups(symbol_infos)
  symbols_at_multiple_addresses = _MultipleAddressSymbols(
    symbol_infos, name_to_group
  )

  # Verify that offsets of the symbols from the orderfile appear in the binary
  # in non-decreasing order.
  last_offset = 0
  name_to_offset = {si.name: si.offset for si in symbol_infos}
  missing_count = 0
  missing_syms = []
  misorder_count = 0
  misordered_syms = []
  for sym in orderfile_symbols:
    if sym in symbols_at_multiple_addresses:
      logging.warning(
        'Ignoring symbol from orderfile appearing '
        + 'at multiple addresses: %s',
        sym,
      )
      continue
    if sym not in name_to_group:
      missing_count += 1
      missing_syms.append(sym)
      continue
    next_offset = name_to_offset[sym]
    group = name_to_group[sym]
    if group.used:
      # Once a group of offsets appears in the orderfile, all symbols from the
      # same group further down in the orderfile are ignored.
      continue
    group.used = True
    next_offset = group.offset
    if next_offset < last_offset:
      misorder_count += 1
      misordered_syms.append((sym, next_offset, last_offset))
    last_offset = next_offset

  # Report missing and incorrectly ordered symbols.
  if missing_count:
    # TODO(crbug.com/340534475): Return False when too many symbols are
    # missing.
    logging.warning(
      '%d missing symbols:\n%s',
      missing_count,
      '\n'.join(str(x) for x in missing_syms[:100]),
    )
  if misorder_count:
    logging.warning(
      '%d misordered symbols:\n%s',
      misorder_count,
      '\n'.join(str(x) for x in misordered_syms[:threshold]),
    )
    if misorder_count > threshold:
      logging.error(
        '%d misordered symbols over threshold %d, failing',
        misorder_count,
        threshold,
      )
      return False
  return True


def ExtractAndVerifySymbolOrder(
  binary_filename: str,
  orderfile_filename: str,
  threshold: int = _DEFAULT_THRESHOLD,
):
  """Extracts symbols and verifies the ordering.

  Args:
    binary_filename: Path to the binary.
    orderfile_filename: Path to the orderfile.
    threshold: The number of misordered symbols beyond which we error.

  Returns:
    True iff the ordering is consistent within |threshold|.
  """
  symbol_infos = symbol_extractor.SymbolInfosFromBinary(binary_filename)
  with open(orderfile_filename, 'r') as f:
    orderfile_symbols = [s.strip() for s in f]
  return _VerifySymbolOrder(orderfile_symbols, symbol_infos, threshold)


def CreateArgumentParser():
  """Creates and returns the argument parser."""
  parser = argparse.ArgumentParser()
  parser.add_argument(
    '--target-arch',
    dest='arch',
    required=True,
    choices=['arm', 'arm64', 'x86', 'x64'],
    help='The target architecture for which to build.',
  )
  parser.add_argument(
    '-C',
    '--out-dir',
    type=pathlib.Path,
    required=True,
    help='Path to the output directory.',
  )
  parser.add_argument(
    '--orderfile-path', required=True, help='Path to the orderfile to verify.'
  )
  parser.add_argument(
    '--threshold',
    action='store',
    dest='threshold',
    default=_DEFAULT_THRESHOLD,
    type=int,
    help='The maximum allowed number of out-of-order symbols.',
  )
  parser.add_argument(
    '-v',
    '--verbose',
    dest='verbosity',
    action='count',
    default=0,
    help='Increase verbosity for debugging.',
  )
  return parser


def main():
  parser = CreateArgumentParser()
  options = parser.parse_args()
  if options.verbosity >= 1:
    level = logging.DEBUG
  else:
    level = logging.INFO
  logging.basicConfig(
    level=level, format='%(levelname).1s %(relativeCreated)6d %(message)s'
  )

  lib_chrome_so = orderfile_shared.GetLibchromeSoPath(
    options.out_dir, options.arch
  )
  if ExtractAndVerifySymbolOrder(
    lib_chrome_so, options.orderfile_path, options.threshold
  ):
    return 0
  return 1


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