# 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.

# Defines fuzzer_test.
#
import("//build/config/sanitizers/sanitizers.gni")
import("//testing/test.gni")

# fuzzer_test is used to define individual libfuzzer tests.
#
# Supported attributes:
# - (required) sources - fuzzer test source files
# - deps - test dependencies
# - libs - Additional libraries to link.
# - frameworks - Apple-only. Additional frameworks to link.
# - additional_configs - additional configs to be used for compilation
# - dict - a dictionary file for the fuzzer.
# - dict_deps - targets that generate the dictionary file, if applicable.
# - libfuzzer_options - options for the fuzzer (e.g. close_fd_mask=N).
#   These are mostly applied only when the fuzzer is being run using the
#   libfuzzer fuzzing engine, but a few of these may be interpreted by
#   other fuzzing engines too
# - centipede_options - options for the fuzzer (e.g. shmem_size_mb=N)
#   when running using the centipede fuzzing engine.
# - asan_options - AddressSanitizer options (e.g. allow_user_segv_handler=1).
# - msan_options - MemorySanitizer options.
# - ubsan_options - UndefinedBehaviorSanitizer options.
# - seed_corpus - a directory with seed corpus.
# - seed_corpus_deps - dependencies for generating the seed corpus.
# - grammar_options - defines a grammar used by a grammar based mutator.
# - exclude_main - if you're going to provide your own 'main' function
# - high_end_job_required - whether the fuzzer requires bigger machines to run.
#   Optional argument.
# - is_fuzzilli_compatible - whether the fuzzer is compatible with fuzzilli.
# - is_fuzztest_compatible - whether the fuzzer is compatible with the fuzztest
#   wrapper when building with centipede. Defaults to true.
#
# If use_libfuzzer gn flag is defined, then proper fuzzer would be build.
# Without use_libfuzzer, use_centipede, or use_chromium_fuzzilli a
# unit-test style binary would be built on linux and the whole target is a
# no-op otherwise.
#
# The template wraps test() target with appropriate dependencies.
# If any test run-time options are present (dict or libfuzzer_options), then a
# config (.options file) file would be generated or modified in root output
# dir (next to test).
template("fuzzer_test") {
  _additional_configs = []
  if (defined(invoker.additional_configs)) {
    _additional_configs = invoker.additional_configs
  }

  # This ensures that the target name is suffixed by `_fuzzer` if
  # "//testing/libfuzzer:no_clusterfuzz" cannot be found in
  # `additional_configs`.
  # This statement is a bit tricky. Basically:
  # 1. We first check whether additional_configs contains
  # "//testing/libfuzzer:no_clusterfuzz". To do that, we simply use
  # `filter_include` and use "//testing/libfuzzer:no_clusterfuzz" as the
  # pattern for an exact match. If the list isn't empty, that means that the
  # configuration was added. Otherwise, we'll use this fuzzer on ClusterFuzz
  # and we must check whether the fuzzer name ends with `_fuzzer` (2.).
  # 2. We use `filter_exclude` with `*_fuzzer` as a pattern and check that
  # against `target_name`. If the list is empty, that means the fuzzer matches
  # the pattern correctly. Note that we can't use `label_matches` here because
  # it only accepts wildcard as a suffix.
  assert(filter_include(_additional_configs,
                        [ "//testing/libfuzzer:no_clusterfuzz" ]) != [] ||
             filter_exclude([ target_name ], [ "*_fuzzer" ]) == [],
         "$target_name name must be suffixed by `_fuzzer`")
  _high_end_job_required = false
  if (defined(invoker.high_end_job_required) && invoker.high_end_job_required) {
    # Target should run in pure release mode, i.e. no debug and no dchecks.
    _high_end_job_required = !is_debug && !dcheck_always_on
  }
  if (defined(invoker.high_end_debug_job_required) &&
      invoker.high_end_debug_job_required) {
    # Target should run in debug'ish mode, i.e. with debug or dchecks.
    _high_end_job_required =
        _high_end_job_required || is_debug || dcheck_always_on
  }

  # If the job is a high_end job and that we are currently building a high_end
  # target, we should compile this fuzzer_test in. Otherwise, for now, we just
  # compile everything in.
  # TODO(crbug.com/333831251): Once CF supports high end jobs, do not compile
  # high end targets except for high end jobs.
  _should_build = (_high_end_job_required || !high_end_fuzzer_targets) &&
                  !disable_libfuzzer && use_fuzzing_engine &&
                  ((defined(invoker.is_fuzzilli_compatible) &&
                    invoker.is_fuzzilli_compatible) || !use_chromium_fuzzilli)
  if (_should_build) {
    assert(defined(invoker.sources), "Need sources in $target_name.")

    test_deps = []
    _exclude_main = defined(invoker.exclude_main) && invoker.exclude_main

    _is_fuzztest_compatible = true
    if (defined(invoker.is_fuzztest_compatible)) {
      _is_fuzztest_compatible = invoker.is_fuzztest_compatible
    }

    # Don't apply the wrapper to any targets that specify a seed corpus or dict
    # because they are not supported by the wrapper and will be manually
    # migrated to FUZZ_TEST. See crbug.com/505034799 for more details.
    _use_fuzztest_llvm_wrapper =
        _is_fuzztest_compatible && use_centipede && !_exclude_main &&
        !defined(invoker.seed_corpus) && !defined(invoker.dict)

    # If using centipede and able to define a main function, wrap the LLVM
    # fuzzer with fuzztest's wrapper and build it as a "native" fuzztest.
    if (_use_fuzztest_llvm_wrapper) {
      test_deps += [ "//third_party/fuzztest:llvm_wrapper" ]
    } else if (_exclude_main) {
      test_deps += [ "//testing/libfuzzer:fuzzing_engine_no_main" ]
    } else {
      test_deps += [ "//testing/libfuzzer:fuzzing_engine_main" ]
    }
    test_data_deps = []
    test_data = []

    if (defined(invoker.deps)) {
      test_deps += invoker.deps
    }
    if (defined(invoker.data_deps)) {
      test_data_deps += invoker.data_deps
    }
    if (defined(invoker.data)) {
      test_data += invoker.data
    }

    supporting_file_test_deps = []
    supporting_file_test_data_deps = []

    if (defined(invoker.seed_corpus) || defined(invoker.seed_corpuses)) {
      assert(!(defined(invoker.seed_corpus) && defined(invoker.seed_corpuses)),
             "Do not use both seed_corpus and seed_corpuses for $target_name.")

      out = "$root_build_dir/$target_name" + "_seed_corpus.zip"

      seed_corpus_deps = []

      if (defined(invoker.seed_corpus_deps)) {
        seed_corpus_deps += invoker.seed_corpus_deps
      }

      action(target_name + "_seed_corpus") {
        script = "//testing/libfuzzer/archive_corpus.py"

        testonly = true

        args = [
          "--output",
          rebase_path(out, root_build_dir),
        ]

        if (defined(invoker.seed_corpus)) {
          args += [ rebase_path(invoker.seed_corpus, root_build_dir) ]
        }

        if (defined(invoker.seed_corpuses)) {
          foreach(seed_corpus_path, invoker.seed_corpuses) {
            args += [ rebase_path(seed_corpus_path, root_build_dir) ]
          }
        }

        outputs = [ out ]

        deps = [ "//testing/libfuzzer:seed_corpus" ] + seed_corpus_deps
      }

      if (archive_seed_corpus) {
        test_data_deps += [ ":" + target_name + "_seed_corpus" ]
        supporting_file_test_deps += [ ":" + target_name + "_seed_corpus" ]
      }
    }

    if (defined(invoker.dict) || defined(invoker.libfuzzer_options) ||
        defined(invoker.centipede_options) || defined(invoker.asan_options) ||
        defined(invoker.msan_options) || defined(invoker.ubsan_options) ||
        defined(invoker.grammar_options)) {
      if (defined(invoker.dict)) {
        # Copy dictionary to output.
        copy(target_name + "_dict_copy") {
          sources = [ invoker.dict ]
          outputs = [ "$root_build_dir/" + target_name + ".dict" ]
          if (defined(invoker.dict_deps)) {
            deps = invoker.dict_deps
          }
        }
        test_data_deps += [ ":" + target_name + "_dict_copy" ]
        supporting_file_test_deps += [ ":" + target_name + "_dict_copy" ]
      }

      fuzzer_name = target_name

      # Generate .options file.
      config_file_name = target_name + ".options"
      action(config_file_name) {
        script = "//testing/libfuzzer/gen_fuzzer_config.py"
        args = [
          "--config",
          rebase_path("$root_build_dir/" + config_file_name, root_build_dir),
        ]

        if (defined(invoker.dict)) {
          args += [
            "--dict",
            rebase_path("$root_build_dir/" + fuzzer_name + ".dict",
                        root_build_dir),
          ]
        } else {
          not_needed([ "fuzzer_name" ])
        }

        if (defined(invoker.libfuzzer_options)) {
          args += [ "--libfuzzer_options" ]
          args += invoker.libfuzzer_options
        }

        if (defined(invoker.centipede_options)) {
          args += [ "--centipede_options" ]
          args += invoker.centipede_options
        }

        if (defined(invoker.asan_options)) {
          args += [ "--asan_options" ]
          args += invoker.asan_options
        }

        if (defined(invoker.msan_options)) {
          args += [ "--msan_options" ]
          args += invoker.msan_options
        }

        if (defined(invoker.ubsan_options)) {
          args += [ "--ubsan_options" ]
          args += invoker.ubsan_options
        }

        if (defined(invoker.grammar_options)) {
          args += [ "--grammar_options" ]
          args += invoker.grammar_options
        }

        outputs = [ "$root_build_dir/$config_file_name" ]
      }
      test_data_deps += [ ":" + config_file_name ]
      supporting_file_test_deps += [ ":" + config_file_name ]
    }

    if (generate_fuzzer_owners) {
      # Generating owners files is slow, only enable when fuzzing engine is
      # used.
      owners_file_name = target_name + ".owners"
      action(owners_file_name) {
        script = "//testing/libfuzzer/gen_fuzzer_owners.py"
        pool = "//testing/libfuzzer:fuzzer_owners_pool"
        args = [
          "--owners",
          rebase_path("$root_build_dir/" + owners_file_name, root_build_dir),
        ]

        if (defined(invoker.sources) && invoker.sources != []) {
          args += [ "--sources" ] + rebase_path(invoker.sources, "//")
        } else if (defined(invoker.deps) && invoker.deps != []) {
          _full_deps = []
          foreach(_dep, invoker.deps) {
            _full_deps += [ get_label_info(_dep, "dir") + ":" +
                            get_label_info(_dep, "name") ]
          }
          args += [
                    "--build-dir",
                    rebase_path("$root_build_dir/", root_build_dir),
                    "--deps",
                  ] + _full_deps
        }

        outputs = [ "$root_build_dir/$owners_file_name" ]
      }
      supporting_file_test_data_deps += [ ":" + owners_file_name ]
    }

    # Copy to executable folder and codesign for catalyst
    if (is_ios) {
      # iOS only supports fuzzer in catalyst environment.
      assert(target_environment == "catalyst")

      # Loop over two kinds of deps to codesign these in two |action_foreach|s.
      # Use the "test_deps", "test_data_deps" as identifiers in for loop.
      foreach(_deps_type_label,
              [
                "test_deps",
                "test_data_deps",
              ]) {
        _dep_list = []
        if (_deps_type_label == "test_deps") {
          _dep_list = supporting_file_test_deps
        } else {
          _dep_list = supporting_file_test_data_deps
        }
        _files_to_sign = []
        foreach(dep, _dep_list) {
          _files_to_sign += get_target_outputs(dep)
        }
        if (_files_to_sign != []) {
          _codesign_action_name =
              target_name + "_codesign_supporting_files_" + _deps_type_label
          action_foreach(_codesign_action_name) {
            testonly = true
            script = "//build/config/apple/codesign.py"
            sources = _files_to_sign
            _codesign_output_path =
                "${root_build_dir}/codesign/{{source_file_part}}"
            outputs = [ _codesign_output_path ]
            args = [
              "code-sign-file",
              "--identity=" + ios_code_signing_identity,
              "--output=" + rebase_path(_codesign_output_path, root_build_dir),
              "{{source}}",
            ]
            deps = _dep_list
          }
          _bundle_data_name = target_name + "_bundle_data_" + _deps_type_label
          bundle_data(_bundle_data_name) {
            testonly = true
            sources = get_target_outputs(":${_codesign_action_name}")
            outputs = [ "{{bundle_executable_dir}}/{{source_file_part}}" ]
            public_deps = [ ":${_codesign_action_name}" ]
          }
          if (_deps_type_label == "test_deps") {
            test_deps += [ ":${_bundle_data_name}" ]
          } else {
            test_data_deps += [ ":${_bundle_data_name}" ]
          }
        }
      }
    } else {
      test_deps += supporting_file_test_deps
      test_data_deps += supporting_file_test_data_deps
    }

    _template_type = "test"
    target(_template_type, target_name) {
      forward_variables_from(invoker,
                             [
                               "cflags",
                               "cflags_cc",
                               "check_includes",
                               "defines",
                               "include_dirs",
                               "output_name",
                               "sources",
                               "libs",
                               "frameworks",
                             ])
      deps = test_deps
      if (_use_fuzztest_llvm_wrapper) {
        fuzztests = [ "LLVMFuzzer.TestOneInput" ]
      }
      data_deps = test_data_deps
      data = test_data
      testonly = true

      configs += _additional_configs
      configs += [ "//testing/libfuzzer:fuzzer_test_config" ]

      # TODO(crbug.com/40031409): Fix code that adds exit-time destructors and
      # enable the diagnostic by removing this line.
      configs += [ "//build/config/compiler:no_exit_time_destructors" ]

      # Used by WebRTC to suppress some Clang warnings in their codebase.
      if (defined(invoker.suppressed_configs)) {
        configs -= invoker.suppressed_configs
      }

      if (defined(invoker.generated_sources)) {
        sources += invoker.generated_sources
      }

      if (is_ios) {
        info_plist =
            "//testing/libfuzzer/fuzzer_support_ios/fuzzer-engine-Info.plist"
      }

      # Depend on libfuzzer_exports to satisfy GN header checking for
      # fuzzers that include libfuzzer_exports.h.
      deps += [ "//testing/libfuzzer:libfuzzer_exports" ]
    }
  } else {
    # noop on unsupported platforms.
    # mark attributes as used.
    not_needed(invoker, "*")
    not_needed(invoker,
               [
                 "deps",
                 "seed_corpus",
                 "sources",
               ])

    group(target_name) {
    }
  }
}
