# Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is also distributed with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms,
# as designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have included with MySQL.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

INCLUDE(CMakePushCheckState)

# Only supported for Clang/llvm
IF(NOT MY_COMPILER_IS_CLANG)
  RETURN()
ENDIF()

# ld.lld: error:
# /usr/lib64/clang/7.0.1/lib/linux/libclang_rt.fuzzer-x86_64.a
# (FuzzerLoop.cpp.o): unsupported SHT_GROUP format
IF(USE_LD_LLD AND C_LD_LLD_RESULT AND CXX_LD_LLD_RESULT)
  STRING(REPLACE "-fuse-ld=lld" ""
    CMAKE_C_LINK_FLAGS ${CMAKE_C_LINK_FLAGS})
  STRING(REPLACE "-fuse-ld=lld" ""
    CMAKE_CXX_LINK_FLAGS ${CMAKE_CXX_LINK_FLAGS})
  STRING(REPLACE "-Wl,--gdb-index" ""
    CMAKE_C_LINK_FLAGS ${CMAKE_C_LINK_FLAGS})
  STRING(REPLACE "-Wl,--gdb-index" ""
    CMAKE_CXX_LINK_FLAGS ${CMAKE_CXX_LINK_FLAGS})
ENDIF()

# check if clang knows about the coverage and trace-pc-guard
#
# compiler  | CFLAGS                             | LDFLAGS
# ----------|------------------------------------|------------------
# llvm 6.0+ | -fsanitize=fuzzer                  | -fsanitize=fuzzer
# llvm 4.0  | -fsanitize-coverage=trace-pc-guard | -lFuzzer
# llvm 3.9  | -fsanitize-coverage=trace-cmp      | -lFuzzer
# llvm 3.7  | -fsanitize-coverage=edge           | -lFuzzer

# llvm 4.0+
CMAKE_PUSH_CHECK_STATE(RESET)
SET(CMAKE_REQUIRED_FLAGS "-fsanitize-coverage=trace-pc-guard")
CHECK_CXX_COMPILER_FLAG("-fsanitize-coverage=trace-pc-guard"
  COMPILER_HAS_SANITIZE_COVERAGE_TRACE_PC_GUARD)
CMAKE_POP_CHECK_STATE()

# llvm 3.8+
CMAKE_PUSH_CHECK_STATE(RESET)
SET(CMAKE_REQUIRED_FLAGS "-fsanitize-coverage=edge")
CHECK_CXX_COMPILER_FLAG("-fsanitize-coverage=edge"
  COMPILER_HAS_SANITIZE_COVERAGE_TRACE_EDGE)
CMAKE_POP_CHECK_STATE()

# http://llvm.org/docs/LibFuzzer.html#tracing-cmp-instructions
CMAKE_PUSH_CHECK_STATE(RESET)
SET(CMAKE_REQUIRED_FLAGS "-fsanitize-coverage=trace-cmp")
CHECK_CXX_COMPILER_FLAG("-fsanitize-coverage=trace-cmp"
  COMPILER_HAS_SANITIZE_COVERAGE_TRACE_CMP)
CMAKE_POP_CHECK_STATE()

CMAKE_PUSH_CHECK_STATE(RESET)
CHECK_CXX_COMPILER_FLAG("-fprofile-instr-generate"
  COMPILER_HAS_PROFILE_INSTR_GENERATE)
CMAKE_POP_CHECK_STATE()

CMAKE_PUSH_CHECK_STATE(RESET)
# invalid argument '-fcoverage-mapping' only allowed
# with '-fprofile-instr-generate'
SET(CMAKE_REQUIRED_FLAGS "-fprofile-instr-generate")
CHECK_CXX_COMPILER_FLAG("-fcoverage-mapping" COMPILER_HAS_COVERAGE_MAPPING)
CMAKE_POP_CHECK_STATE()

CMAKE_PUSH_CHECK_STATE(RESET)
SET(CMAKE_REQUIRED_LIBRARIES "-fsanitize=fuzzer")
SET(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer")
CHECK_CXX_SOURCE_COMPILES("
extern \"C\" int LLVMFuzzerTestOneInput (void *, int)
{ return 0; }"
COMPILER_HAS_SANITIZE_FUZZER)
CMAKE_POP_CHECK_STATE()


IF(COMPILER_HAS_SANITIZE_FUZZER)
  SET(SANITIZE_COVERAGE_FLAGS "-fsanitize=fuzzer")
ELSEIF(COMPILER_HAS_SANITIZE_COVERAGE_TRACE_PC_GUARD)
  SET(SANITIZE_COVERAGE_FLAGS "-fsanitize-coverage=trace-pc-guard")
ELSEIF(COMPILER_HAS_SANITIZE_COVERAGE_TRACE_EDGE)
  SET(SANITIZE_COVERAGE_FLAGS "-fsanitize-coverage=edge")
ENDIF()

# check that libFuzzer is found
#
# check_library_exists() doesn't work here as it would provide a main() which
# calls a test-function ... which collides with libFuzzer's main():
#
# if the libFuzzer is found by the compiler it will provide a 'main()' and
# require that we provide a 'LLVMFuzzerTestOneInput' at link-time.
IF(SANITIZE_COVERAGE_FLAGS)
  CMAKE_PUSH_CHECK_STATE(RESET)
  SET(CMAKE_REQUIRED_LIBRARIES Fuzzer)
  SET(CMAKE_REQUIRED_FLAGS ${SANITIZE_COVERAGE_FLAGS})
  CHECK_CXX_SOURCE_COMPILES("
      extern \"C\" int LLVMFuzzerTestOneInput (void *, int)
      { return 0; }"
      CLANG_HAS_LIBFUZZER)
  CMAKE_POP_CHECK_STATE()
ENDIF()

IF(COMPILER_HAS_SANITIZE_FUZZER OR CLANG_HAS_LIBFUZZER)
  IF(CLANG_HAS_LIBFUZZER)
    SET(LIBFUZZER_LIBRARIES Fuzzer)
  ENDIF()
  SET(LIBFUZZER_LINK_FLAGS ${SANITIZE_COVERAGE_FLAGS})
  SET(LIBFUZZER_COMPILE_FLAGS)
  LIST(APPEND LIBFUZZER_COMPILE_FLAGS ${SANITIZE_COVERAGE_FLAGS})

  IF(COMPILER_HAS_PROFILE_INSTR_GENERATE)
    LIST(APPEND LIBFUZZER_COMPILE_FLAGS -fprofile-instr-generate)
    SET(LIBFUZZER_LINK_FLAGS
      "${LIBFUZZER_LINK_FLAGS} -fprofile-instr-generate")
    LIST(APPEND LIBFUZZER_COMPILE_FLAGS -fcoverage-mapping)
  ENDIF()
ENDIF()


FUNCTION(LIBFUZZER_ADD_TEST TARGET)
  SET(OPTS)
  SET(ONE_VAL_ARGS INITIAL_CORPUS_DIR MAX_TOTAL_TIME TIMEOUT)
  SET(MULTI_VAL_ARGS)
  CMAKE_PARSE_ARGUMENTS(ARG "${OPTS}" "${ONE_VAL_ARGS}" "${MULTI_VAL_ARGS}" ${ARGN})


  IF(NOT DEFINED ARG_MAX_TOTAL_TIME)
    SET(ARG_MAX_TOTAL_TIME 10)
  ENDIF()
  IF(NOT DEFINED ARG_TIMEOUT)
    SET(ARG_TIMEOUT 0.1)
  ENDIF()

  SET(BINARY_CORPUS_DIR "${CMAKE_CURRENT_BINARY_DIR}/corpus/${TARGET}")
  SET(BINARY_ARTIFACT_DIR "${CMAKE_CURRENT_BINARY_DIR}/artifacts/${TARGET}")

  SET_TARGET_PROPERTIES(
    ${TARGET}
    PROPERTIES
    COMPILE_OPTIONS "${LIBFUZZER_COMPILE_FLAGS}"
    LINK_FLAGS "${LIBFUZZER_LINK_FLAGS}"
    )

  IF(LIBFUZZER_LIBRARIES)
    TARGET_LINK_LIBRARIES(${TARGET} ${LIBFUZZER_LIBRARIES})
  ENDIF()

  # reset the artifact and corpus dir if the binary changed.
  ADD_CUSTOM_COMMAND(TARGET ${TARGET}
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E remove_directory "${BINARY_CORPUS_DIR}"
    COMMAND ${CMAKE_COMMAND} -E make_directory "${BINARY_CORPUS_DIR}"
    COMMAND ${CMAKE_COMMAND} -E remove_directory "${BINARY_ARTIFACT_DIR}"
    COMMAND ${CMAKE_COMMAND} -E make_directory "${BINARY_ARTIFACT_DIR}"
  )

  IF(ARG_INITIAL_CORPUS_DIR)
    # prepare the corpus in the build-dir based on samples from the source-dir
    ADD_CUSTOM_COMMAND(TARGET ${TARGET}
      POST_BUILD
      COMMAND $<TARGET_FILE:${TARGET}>
        -merge=1
        -verbosity=0
        -merge_control_file="${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.control"
        "${ARG_INITIAL_CORPUS_DIR}" "${BINARY_CORPUS_DIR}" 2> /dev/null
      COMMENT "Preparing corpus for ${TARGET}"
      )
  ENDIF()

  # use cmake -E env to set the LLVM_PROFILE_FILE in a portable way
  ADD_TEST(${TARGET}
            ${TARGET}
            -max_total_time=${ARG_MAX_TOTAL_TIME} -timeout=${ARG_TIMEOUT}
            -artifact_prefix=${BINARY_ARTIFACT_DIR}/
            ${BINARY_CORPUS_DIR}
  )
ENDFUNCTION()
