#!/usr/bin/env python3

# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Script for generating .proto and a conversion .cc file for a templated library
based JavaScript parser fuzzer.
"""

import sys


def ParseWord(word_string):
  # Every part of the word is either a string surrounded by "" or a placeholder
  # $<int>.

  word_string = word_string.lstrip().rstrip()

  parts = []
  while len(word_string) > 0:
    if word_string[0] == '"':
      end_ix = 1 + word_string[1:].index('"')
      parts.append(word_string[1:end_ix])
      word_string = word_string[(end_ix + 1):]
    elif word_string[0] == '$':
      if ' ' in word_string:
        end_ix = word_string.index(' ')
      else:
        end_ix = len(word_string)
      parts.append(int(word_string[1:end_ix]))
      word_string = word_string[end_ix:]
    else:
      assert False
    word_string = word_string.lstrip()
  return parts


def GenerateProtoContents(words):
  contents = ''
  for ix in range(len(words)):
    contents += '    token_value_' + str(ix) + ' = ' + str(ix) + ';\n'
  return contents


def GenerateConversionContents(words):
  contents = ''
  ix = 0
  for word in words:
    contents += '    case ' + str(ix) + ':\n'
    max_part = -1
    first = True
    building_string = ''
    for part in word:
      if not first:
        building_string += ' + std::string(" ") + '
      if isinstance(part, str):
        building_string += 'std::string("' + part + '")'
      else:
        if part > max_part:
          max_part = part
        building_string += ('token_to_string(token.inner_tokens(' + str(part) +
                            '), depth)')
      first = False
    if max_part >= 0:
      contents += ('      if (token.inner_tokens().size() < ' +
                   str(max_part + 1) + ') return std::string("");\n')
    contents += '      return ' + building_string + ';\n'
    ix += 1
  return contents


def ReadDictionary(filename):
  with open(filename) as input_file:
    lines = input_file.readlines()
  words = []
  for line in lines:
    if not line.startswith('#'):
      word = ParseWord(line)
      if len(word) > 0:
        words.append(word)
  return words


def main(argv):
  output_proto_file = argv[1]
  output_cc_file = argv[2]
  input_dict_file = argv[3]

  words = ReadDictionary(input_dict_file)

  proto_header = ('// Generated by generate_javascript_parser_proto.py.\n'
                  '\n'
                  'syntax = "proto2";\n'
                  'package javascript_parser_proto_fuzzer;\n'
                  '\n'
                  'message Token {\n'
                  '  enum Value {\n')

  proto_footer = ('  }\n'
                  '  required Value value = 1;\n'
                  '  repeated Token inner_tokens = 2;\n'
                  '}\n'
                  '\n'
                  'message Source {\n'
                  '  required bool is_module = 1;\n'
                  '  repeated Token tokens = 2;\n'
                  '}\n')

  proto_contents = proto_header + GenerateProtoContents(words) + proto_footer

  with open(output_proto_file, 'w') as f:
    f.write(proto_contents)

  conversion_header = (
      '// Generated by generate_javascript_parser_proto.py.\n'
      '\n'
      '#include "testing/libfuzzer/fuzzers/'
      'javascript_parser_proto_to_string.h"\n'
      '\n'
      '// Bound calls to token_to_string to prevent memory usage from growing\n'
      '// too much.\n'
      'const int kMaxRecursiveDepth = 9;\n'
      '\n'
      'std::string token_to_string(\n'
      '    const javascript_parser_proto_fuzzer::Token& token, int depth)'
      ' {\n'
      '  if (++depth == kMaxRecursiveDepth) return std::string("");\n'
      '  switch(token.value()) {\n')

  conversion_footer = ('    default: break;\n'
                       '  }\n'
                       '  return std::string("");\n'
                       '}\n')

  conversion_contents = (conversion_header + GenerateConversionContents(words) +
                         conversion_footer)

  with open(output_cc_file, 'w') as f:
    f.write(conversion_contents)


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