#!/usr/bin/env bash
# Parallel fuzz runner + stats for source2rosetta — unifies the run + report steps.
#
# Fans all offline-derivation fuzz targets out across the box with GNU parallel (each target getting
# N libFuzzer workers), tee's per-target logs to a timestamped dir, then prints a coverage/execs/crash
# table. 8 targets x 3 workers = 24 threads by default — above an 8C/16T box's thread count, so pass a
# smaller `workers` there rather than trusting the default (the thread count is computed from TARGETS,
# so it moves when a target is added; this comment is the part that does not).
#
# Usage:
#   ./fuzz.sh [seconds] [workers]   run for `seconds` (default 60) with `workers`/target (default 3),
#                                   then print stats.
#   ./fuzz.sh --stats               skip fuzzing, just re-print stats from the latest run + corpus.
set -euo pipefail

CRATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"   # source2rosetta/ — cargo fuzz runs from here
cd "$CRATE_DIR"

TARGETS=(fuzz_elf fuzz_schema fuzz_rtti fuzz_sig_abi fuzz_xref fuzz_valvetab fuzz_pulse fuzz_concmd)
FUZZ_ROOT="$CRATE_DIR/fuzz"
LOG_BASE="$FUZZ_ROOT/logs"

# Ignore iced_x86's intentional one-time 'static decoder-table allocation (see lsan_suppressions.txt);
# a real leak in our own code still fails the run.
#
# CAVEAT: an LSan suppression matches on SYMBOLIZED frames, so if symbolization stalls — which it can
# under this script's own load, every target symbolizing at once — the frame list comes back bare and the
# suppression misses. That surfaces as a spurious iced_x86 leak artifact under a decoder-heavy target
# (fuzz_xref, fuzz_sig_abi). Before triaging one, replay it single-target: a false positive reports
# "Suppressions used: iced_x86" and exits 0.
export LSAN_OPTIONS="suppressions=$FUZZ_ROOT/lsan_suppressions.txt"

STATS_ONLY=false
if [[ "${1:-}" == "--stats" ]]; then STATS_ONLY=true; fi
SECS="${1:-60}"
WORKERS="${2:-3}"

human() { # human-readable count
  awk -v n="$1" 'BEGIN{if(n>=1e9)printf"%.1fB",n/1e9;else if(n>=1e6)printf"%.1fM",n/1e6;else if(n>=1e3)printf"%.1fK",n/1e3;else printf"%d",n}'
}
parse_sum() { grep -h "stat::$2:" "$1"/*.log 2>/dev/null | awk -F: '{s+=$NF}END{print s+0}'; }
parse_max() { grep -h "stat::$2:" "$1"/*.log 2>/dev/null | awk -F: '{v=$NF+0;if(v>m)m=v}END{print m+0}'; }

run_fuzz() {
  command -v parallel >/dev/null || { echo "ERROR: GNU parallel required." >&2; exit 1; }
  echo "== seeding corpus =="
  (cd fuzz && cargo +nightly run --bin gen_corpus --quiet)
  echo "== building targets =="
  cargo +nightly fuzz build >/dev/null 2>&1

  LOG_DIR="$LOG_BASE/$(date +%Y-%m-%d_%H%M%S)"
  mkdir -p "$LOG_DIR"
  ln -sfn "$(basename "$LOG_DIR")" "$LOG_BASE/latest"
  echo "== fuzzing ${#TARGETS[@]} targets x ${WORKERS} workers = $(( ${#TARGETS[@]} * WORKERS )) threads, ${SECS}s each =="
  echo "   logs: $LOG_DIR"

  # -jobs/-workers=N → N concurrent libFuzzer workers per target; -max_len=65536 so real-ELF seeds
  # aren't truncated to the 4 KiB default. `parallel -j` launches all targets at once.
  printf '%s\n' "${TARGETS[@]}" | parallel -j "${#TARGETS[@]}" --lb --tagstring '{}' \
    "cargo +nightly fuzz run {} -- -max_total_time=${SECS} -jobs=${WORKERS} -workers=${WORKERS} -max_len=65536 -print_final_stats=1 >${LOG_DIR}/{}.log 2>&1; echo {} done"
  # libFuzzer with -jobs writes fuzz-*.log turds into the crate dir; sweep them into the log dir.
  mv fuzz-*.log "$LOG_DIR/" 2>/dev/null || true
  echo "LATEST_LOG=$LOG_DIR"
}

print_stats() {
  local log_dir=""
  [[ -L "$LOG_BASE/latest" ]] && log_dir="$(cd "$LOG_BASE/latest" && pwd)"
  echo
  echo "source2rosetta fuzzing stats  ${log_dir:+(logs: $log_dir)}"
  printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' Target Corpus Size Cov Execs Exec/s Crashes T/O OOM
  printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' --- --- --- --- --- --- --- --- ---
  local tc=0 tx=0 tcr=0 tto=0 too=0
  for t in "${TARGETS[@]}"; do
    local cdir="$FUZZ_ROOT/corpus/$t" adir="$FUZZ_ROOT/artifacts/$t"
    local n=0 bytes=0
    [[ -d "$cdir" ]] && { n=$(find "$cdir" -maxdepth 1 -type f | wc -l); bytes=$(find "$cdir" -maxdepth 1 -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1}END{print s+0}'); }
    local cr=0 to=0 oo=0
    [[ -d "$adir" ]] && { cr=$(find "$adir" -maxdepth 1 -name 'crash-*' | wc -l); to=$(find "$adir" -maxdepth 1 -name 'timeout-*' | wc -l); oo=$(find "$adir" -maxdepth 1 -name 'oom-*' | wc -l); }
    local execs="-" execps="-" cov="-"
    if [[ -n "$log_dir" && -f "$log_dir/$t.log" ]]; then
      execs=$(human "$(grep -h 'stat::number_of_executed_units:' "$log_dir/$t.log" 2>/dev/null | awk -F: '{s+=$NF}END{print s+0}')")
      execps=$(human "$(grep -h 'stat::average_exec_per_sec:' "$log_dir/$t.log" 2>/dev/null | awk -F: '{v=$NF+0;if(v>m)m=v}END{print m+0}')")
    fi
    # coverage: replay the corpus once (-runs=0). Skipped when empty.
    if (( n > 0 )); then
      cov=$(cargo +nightly fuzz run "$t" "fuzz/corpus/$t" -- -runs=0 -max_len=65536 2>&1 | grep -oP 'cov: \K[0-9]+' | tail -1 || true)
      cov="${cov:--}"
    fi
    printf '%-16s %7d %8s %7s %9s %8s %8d %5d %4d\n' "$t" "$n" "$(human "$bytes")B" "$cov" "$execs" "$execps" "$cr" "$to" "$oo"
    tc=$((tc+n)); tcr=$((tcr+cr)); tto=$((tto+to)); too=$((too+oo))
  done
  printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' --- --- --- --- --- --- --- --- ---
  printf '%-16s %7d %8s %7s %9s %8s %8d %5d %4d\n' "TOTAL" "$tc" "" "" "" "" "$tcr" "$tto" "$too"
  if (( tcr + tto + too > 0 )); then
    echo; echo "!! artifacts found — triage: cargo +nightly fuzz tmin <target> <fuzz/artifacts/<target>/crash-...>"
    find "$FUZZ_ROOT/artifacts" -type f 2>/dev/null | sed 's/^/   /'
  else
    echo; echo "no crashes / timeouts / OOMs — the offline derivation held on every explored input."
  fi
}

$STATS_ONLY || run_fuzz
print_stats
