#!/usr/bin/env python3
# Copyright 2017 The PDFium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Verifies exported functions in public/*.h are in fpdf_view_c_api_test.c.

This script gathers a list of functions from public/*.h that contain
FPDF_EXPORT. It then gathers a list of functions from
fpdfsdk/fpdf_view_c_api_test.c. It then verifies both lists do not contain
duplicates, and they match each other. It also checks that the order in
fpdf_view_c_api_test.c is alphabetical within each section.

"""

import os
import re
import sys


def _IsValidFunctionName(function, filename):
  if function.startswith('FPDF'):
    return True
  if function.startswith('FSDK_') and filename == 'fpdf_ext.h':
    return True
  if function.startswith('FORM_') and filename == 'fpdf_formfill.h':
    return True
  return False


def _FindFunction(function_snippet, filename):
  function_split = function_snippet.split('(')
  assert len(function_split) == 2
  function = function_split[0]
  assert _IsValidFunctionName(function, filename)
  return function


def _GetExportsFromHeader(dirname, filename):
  with open(os.path.join(dirname, filename)) as f:
    contents = f.readlines()
    look_for_function_name = False
    functions = []
    for line in contents:
      if look_for_function_name:
        look_for_function_name = False
        split_line = line.rstrip().split(' ')
        functions.append(_FindFunction(split_line[0], filename))
        continue

      if not line.startswith('FPDF_EXPORT '):
        continue

      # Format should be: FPDF_EXPORT return_type FPDF_CALLCONV
      split_line = line.rstrip().split(' ')
      callconv_index = split_line.index('FPDF_CALLCONV')
      assert callconv_index >= 2
      if callconv_index + 1 == len(split_line):
        look_for_function_name = True
        continue

      functions.append(_FindFunction(split_line[callconv_index + 1], filename))
    return functions


def _GetFunctionsFromPublicHeaders(src_path):
  public_path = os.path.join(src_path, 'public')
  functions = []
  for filename in os.listdir(public_path):
    if filename.endswith('.h'):
      functions.extend(_GetExportsFromHeader(public_path, filename))
  return functions


def _CheckSorted(functions):
  unsorted_functions = set()
  for i in range(len(functions) - 1):
    if functions[i] > functions[i + 1]:
      unsorted_functions.add(functions[i])
      unsorted_functions.add(functions[i + 1])
  return unsorted_functions


def _GetFunctionsFromTest(api_test_path):
  chk_regex = re.compile('^    CHK\((.*)\);\n$')
  file_regex = re.compile('^    //.*\.h\n$')
  with open(api_test_path) as f:
    contents = f.readlines()
    functions = []
    functions_in_file = []
    for line in contents:
      if (file_regex.match(line)):
        functions.append(functions_in_file)
        functions_in_file = []
      match = chk_regex.match(line)
      if match:
        functions_in_file.append(match.groups()[0])
    functions.append(functions_in_file)
    return functions


def _FindDuplicates(functions):
  return {f for f in functions if functions.count(f) > 1}


def _CheckAndPrintFailures(failure_list, failure_message):
  if not failure_list:
    return True

  print('%s:' % failure_message)
  for f in sorted(failure_list):
    print(f)
  return False


def main():
  script_abspath = os.path.abspath(__file__)
  src_path = os.path.dirname(os.path.dirname(os.path.dirname(script_abspath)))
  public_functions = _GetFunctionsFromPublicHeaders(src_path)

  api_test_relative_path = os.path.join('fpdfsdk', 'fpdf_view_c_api_test.c')
  api_test_path = os.path.join(src_path, api_test_relative_path)
  test_functions_per_section = _GetFunctionsFromTest(api_test_path)
  result = True
  unsorted_functions = set()
  for functions in test_functions_per_section:
    unsorted_functions |= _CheckSorted(functions)
  check = _CheckAndPrintFailures(
      unsorted_functions,
      'Found CHKs that are not in alphabetical order within each section in %s'
      % api_test_path)
  result = result and check

  duplicate_public_functions = _FindDuplicates(public_functions)
  check = _CheckAndPrintFailures(duplicate_public_functions,
                                 'Found duplicate functions in public headers')
  result = result and check

  test_functions = [
      function for functions in test_functions_per_section
      for function in functions
  ]
  duplicate_test_functions = _FindDuplicates(test_functions)
  check = _CheckAndPrintFailures(duplicate_test_functions,
                                 'Found duplicate functions in API test')
  result = result and check

  public_functions_set = set(public_functions)
  test_functions_set = set(test_functions)
  not_tested = public_functions_set.difference(test_functions_set)
  check = _CheckAndPrintFailures(not_tested, 'Functions not tested')
  result = result and check
  non_existent = test_functions_set.difference(public_functions_set)
  check = _CheckAndPrintFailures(non_existent, 'Tested functions do not exist')
  result = result and check

  if not result:
    print('Some checks failed. Make sure %s is in sync with the public API '
          'headers.' % api_test_relative_path)
    return 1

  return 0


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