#!/bin/bash

# Copyright 2023 The Centipede Authors.
#
# 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
#
#      https://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.

# Tests output format of Centipede. Uses regexes to verify if the output
# matches the format expected by ClusterFuzz.

set -eu

source "$(dirname "$0")/../test_util.sh"

# Centipede and target binaries.
declare centipede
centipede="$(centipede::get_centipede_test_srcdir)/centipede"
declare target
target="$(centipede::get_centipede_test_srcdir)/testing/clusterfuzz_format_target"
declare sanitized_target
sanitized_target="$(centipede::get_centipede_test_srcdir)/testing/clusterfuzz_format_sanitized_target"

# Input files.
declare -r oom="${TEST_TMPDIR}/oom"
declare -r uaf="${TEST_TMPDIR}/uaf"
declare -r slo="${TEST_TMPDIR}/slo"

echo -n oom > "${oom}"  # Triggers out-of-memory.
echo -n uaf > "${uaf}"  # Triggers heap-use-after-free.
echo -n slo > "${slo}"  # Triggers heap-use-after-free.

# Shorthand to run centipede with necessary flags.
abort_test_fuzz() {
  set -x
  "${centipede}" \
    --workdir="${WD}" \
    --binary="${target}" --symbolizer_path=/dev/null \
    --extra_binaries="${sanitized_target}" \
    --address_space_limit_mb=4096 \
    --timeout_per_input=5 \
    "$@" 2>&1
  set +x
}

# Tests fuzzing with a target that crashes.
test_crashing_target() {
  local -r input_file="$1"
  local -r expected_regex="$2"
  local -r input_file_basename="$(basename "${input_file}")"
  local -r FUNC="${FUNCNAME[0]}"
  local -r WD="${TEST_TMPDIR}/${FUNC}/WD"
  local -r TMPCORPUS="${TEST_TMPDIR}/${FUNC}/C"
  local -r LOG="${TEST_TMPDIR}/${FUNC}/log_${input_file_basename}"
  centipede::ensure_empty_dir "${WD}"
  centipede::ensure_empty_dir "${TMPCORPUS}"

  # Create a corpus with one crasher and one other input.
  cp "$1" "${TMPCORPUS}"  # Triggers an error.
  echo -n "foo" >"${TMPCORPUS}/foo"     # Just some input.
  abort_test_fuzz --corpus_from_files="${TMPCORPUS}"

  # Run fuzzing with num_runs=0, i.e. only run the inputs from the corpus.
  # Expecting a crash to be observed and reported.
  abort_test_fuzz --num_runs=0 | tee "${LOG}"

  # Sanity check. Validate failure input bytes.
  centipede::assert_regex_in_file \
    "^Input bytes[ \t]*: ${input_file_basename}" "${LOG}"

  # The following formats are required by ClusterFuzz.
  # Validate failure reason format.
  centipede::assert_regex_in_file \
    "^CRASH LOG: ${expected_regex}" "${LOG}"
  # Validate input saving format.
  centipede::assert_regex_in_file \
    '^Saving input to: .\+/crashes/.\+' "${LOG}"
}

# Check if the following crash logs are in the format expected by ClusterFuzz.
# This input triggers ASAN heap-use-after-free error.
echo ======== Check UAF crash log format.
test_crashing_target "${uaf}" '.*ERROR: AddressSanitizer: heap-use-after-free'

# This input triggers out-of-memory error.
echo ======== Check OOM crash log format.
test_crashing_target "${oom}" '========= RSS limit exceeded:'

# This input triggers timeout.
echo ======== Check timeout crash log format.
test_crashing_target "${slo}" '========= Per-input timeout exceeded:'

echo "PASS"
