# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Fuzzer options."""

import configparser
import os
import random
import re

import six

from clusterfuzz._internal.bot.fuzzers import utils as fuzzer_utils
from clusterfuzz._internal.bot.fuzzers.afl import constants as afl_constants
from clusterfuzz._internal.metrics import logs
from clusterfuzz._internal.system import environment

OPTIONS_FILE_EXTENSION = '.options'

# Whitelist for env variables .options files can set.
ENV_VAR_WHITELIST = set([afl_constants.DONT_DEFER_ENV_VAR])


class FuzzerOptionsException(Exception):
  """Exceptions for fuzzer options."""


class FuzzerArguments(object):
  """Fuzzer flags."""

  def __init__(self, flags):
    self.flags = flags

  def __contains__(self, key):
    return key in self.flags

  def __getitem__(self, key):
    return self.flags[key]

  def __setitem__(self, key, value):
    self.flags[key] = value

  def get(self, key, default=None, constructor=None):
    """Return value for |key|, calling the |constructor| on it, or |default| if
    the key does not exist, or if the constructor threw an exception."""
    try:
      value = self[key]
      if constructor:
        value = constructor(value)

      return value
    except Exception:
      pass

    return default

  def dict(self):
    """Return arguments as a dict."""
    return self.flags

  def list(self):
    """Return arguments as a list."""
    return ['-%s=%s' % (key, value) for key, value in six.iteritems(self.flags)]


class FuzzerOptions(object):
  """Represents fuzzer and related options."""

  OPTIONS_RANDOM_REGEX = re.compile(
      r'^\s*random\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*$')

  def __init__(self, options_file_path, cwd=None):
    if not os.path.exists(options_file_path):
      raise FuzzerOptionsException('fuzzer options file does not exist.')

    if cwd:
      self._cwd = cwd
    else:
      self._cwd = os.path.dirname(options_file_path)

    self._config = configparser.ConfigParser()
    with open(options_file_path, 'r') as f:
      try:
        self._config.read_file(f)
      except configparser.Error:
        raise FuzzerOptionsException('Failed to parse fuzzer options file.')

  def _get_dict_path(self, relative_dict_path):
    """Return a full path to the dictionary."""
    return os.path.join(self._cwd, relative_dict_path)

  def _get_option_section(self, section):
    """Get an option section."""
    if not self._config.has_section(section):
      return {}

    return dict(self._config.items(section))

  def get_env(self):
    """Returns dict containing env variables and their values set by "env"
    section. Only includes env variables permitted by |ENV_VAR_WHITELIST|.
    Variables are assumed to contain no lower case letters.
    """
    env = {}
    for var_name, var_value in six.iteritems(self._get_option_section('env')):

      var_name = var_name.upper()
      if var_name in ENV_VAR_WHITELIST:
        env[var_name] = var_value

    return env

  def get_engine_arguments(self, engine):
    """Return a list of fuzzer options."""
    arguments = {}
    for option_name, option_value in six.iteritems(
        self._get_option_section(engine)):
      # Check option value for usage of random() function.
      match = self.OPTIONS_RANDOM_REGEX.match(option_value)
      if match:
        min_value, max_value = match.groups()
        option_value = str(random.SystemRandom().randint(
            int(min_value), int(max_value)))

      if option_name == 'dict':
        option_value = self._get_dict_path(option_value)

      arguments[option_name] = option_value

    return FuzzerArguments(arguments)

  def get_asan_options(self):
    """Return a list of ASAN_OPTIONS overrides."""
    return self._get_option_section('asan')

  def get_msan_options(self):
    """Return a list of MSAN_OPTIONS overrides."""
    return self._get_option_section('msan')

  def get_ubsan_options(self):
    """Return a list of UBSAN_OPTIONS overrides."""
    return self._get_option_section('ubsan')

  def get_hwasan_options(self):
    """Return a list of HWSAN_OPTIONS overrides."""
    return self._get_option_section('hwasan')

  def get_grammar_options(self):
    """Return a list og grammar options"""
    return self._get_option_section('grammar')


def get_fuzz_target_options(fuzz_target_path):
  """Return a FuzzerOptions for the given target, or None if it does not
  exist."""
  options_file_path = fuzzer_utils.get_supporting_file(fuzz_target_path,
                                                       OPTIONS_FILE_EXTENSION)

  if environment.is_trusted_host():
    options_file_path = fuzzer_utils.get_file_from_untrusted_worker(
        options_file_path)

  if not os.path.exists(options_file_path):
    return None

  options_cwd = os.path.dirname(options_file_path)

  try:
    return FuzzerOptions(options_file_path, cwd=options_cwd)
  except FuzzerOptionsException:
    logs.log_error('Invalid options file: %s.' % options_file_path)
    return None
