#
# Copyright (c) 2019 Vinnie Falco (vinnie.falco@gmail.com)
# Copyright (c) 2019 Paul Dreik
# Copyright (c) 2021 Dmitry Arkhipov (grisumbras@gmail.com)
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
# Official repository: https://github.com/boostorg/json
#


import common ;
import link ;
import os ;
import path ;
import property ;
import sequence ;
import testing ;


# set the maximum size of the input, to avoid
# big inputs which blow up the corpus size
.MAXLEN = [ os.environ MAXLEN ] ;
.MAXLEN ?= -max_len=4000 ;

# set a timelimit (you may want to adjust this if you run locally)
.MAXTIME = [ os.environ MAXTIME  ] ;
.MAXTIME ?= -max_total_time=30 ;

# If doing fuzzing locally (not in CI), adjust this to utilize more
# of your cpu.
#JOBS="-jobs=32"
.JOBS = [ os.environ JOBS ] ;

# make sure ubsan stops in case anything is found
.UBSAN_OPTIONS = [
    common.variable-setting-command UBSAN_OPTIONS : halt_on_error=1
] ;

local corpus.tar = [ glob-ex . : corpus.tar ] ;
local test-corpus ;
if $(corpus.tar)
{
    # if an old corpus exists, use it
    make old-corpus
        : $(corpus.tar)
        : @untar-corpus
        : <location>oldcorpus
        ;
    explicit old-corpus ;
}
else
{
    test-corpus = [ glob-tree-ex ../test : *.json ] ;
}

local old-runs = [ SORT [ glob-tree-ex old_crashes : * ] ] ;
make old_crashes : : @mkdir : <location>. ;
explicit old_crashes ;

local variants = basic_parser parse parser direct_parse ;
for local variant in basic_parser parse parser direct_parse
{
    local $(variant)-runs ;
    local fuzzer = fuzzer_$(variant) ;
    exe $(fuzzer)
        : fuzz_$(variant).cpp /boost/json//json_sources
        : requirements
          <toolset>clang
          <conditional>@fuzzer-props
        ;

    # make sure the old crashes pass without problems
    if $(old-runs)
    {
        run $(fuzzer)
            : target-name $(variant)-run-crashes
            : input-files $(old-runs)
            ;
        explicit $(variant)-run-crashes ;
        $(variant)-runs += $(variant)-run-crashes ;
    }

    local old-corpus-deps ;
    if $(corpus.tar)
    {
        old-corpus-deps = old-corpus ;
    }
    else
    {
        # make an initial corpus from the test data already in the repo
        for file in $(test-corpus)
        {
            local copied = $(variant)/$(file:D=) ;
            make $(copied) : $(file) : common.copy : <location>oldcorpus ;
            explicit $(copied) ;
            old-corpus-deps += $(copied) ;
        }
    }
    make oldcorpus/$(variant)
        : $(old-corpus-deps)
        : @mkdir
        : <location>.
        ;
    explicit oldcorpus/$(variant) ;

    # run the fuzzer for a short while
    make out/$(variant)
        : $(fuzzer)
          oldcorpus/$(variant)
          old_crashes
        : @run-fuzzer
        : <location>.
          <flags>$(.MAXTIME)
          <flags>$(.MAXLEN)
          <flags>$(.JOBS)
        ;
    $(variant)-runs += out/$(variant) ;

    # minimize the corpus
    make cmin/$(variant)
        : $(fuzzer)
          out/$(variant)
          oldcorpus/$(variant)
          old_crashes
        : @run-fuzzer
        : <location>.
          <flags>-merge=1
          <flags>$(.MAXLEN)
        ;
    $(variant)-runs += cmin/$(variant) ;

    alias $(variant)-run : $($(variant)-runs) ;
    explicit $($(variant)-runs) ;
}

alias run : $(variants)-run ;
explicit run $(variants)-run ;


rule mkdir ( target : source * : props * )
{
    local dir = [ path.make [ on $(target) return $(LOCATE) ] ] ;
    dir = [ path.join $(dir) $(target:G=) ] ;
    common.MkDir $(dir) ;
}

rule fuzzer-props ( props * )
{
    local toolset = [ property.select toolset : $(props) ] ;
    if clang = $(toolset:G=)
    {
        return
          <debug-symbols>on
          <optimization>speed
          <address-sanitizer>on
          <undefined-sanitizer>norecover
          <cxxflags>-fsanitize=fuzzer
          <linkflags>-fsanitize=fuzzer
          # explicitly set BOOST_JSON_STACK_BUFFER_SIZE small so interesting
          # code paths are taken also for small inputs
          # (see https://github.com/boostorg/json/issues/333)
          <define>BOOST_JSON_STACK_BUFFER_SIZE=64
          ;
    }
    else
    {
        return <build>no ;
    }
}


rule run-fuzzer ( target : sources * : props * )
{
    local flags = [ property.select flags : $(props) ] ;
    FLAGS on $(target) = $(flags:G=) ;

    LOG on $(target) = [ path.native [ path.join $(target) _log ] ] ;

    local dir = [ path.make [ on $(target) return $(LOCATE) ] ] ;
    dir = $(dir)/$(target:G=) ;
    common.MkDir $(dir) ;
    DEPENDS $(target) : $(dir) ;

    LOG on $(target) = [ path.native [ path.join $(dir) _log ] ] ;
}

.SET_STATUS = [ modules.peek testing : .SET_STATUS ] ;
.STATUS = [ modules.peek testing : .STATUS ] ;
.RUN_OUTPUT_NL = [ modules.peek testing : .RUN_OUTPUT_NL ] ;
.STATUS_NOT_0 = [ modules.peek testing : .STATUS_NOT_0 ] ;
.CATENATE = [ modules.peek testing : .CATENATE ] ;
.ENDIF = [ modules.peek testing : .ENDIF ] ;
.NULL_OUT = [ modules.peek common : NULL_OUT ] ;
.RM = [ common.rm-command ] ;
actions run-fuzzer
{
    $(.UBSAN_OPTIONS)
    $(>[1]) $(<) $(>[2-]) $(FLAGS) > "$(LOG)" 2>&1
    $(.SET_STATUS)
    if $(.STATUS_NOT_0)
        echo ====== BEGIN OUTPUT ======
        $(.CATENATE) "$(LOG)"
        echo ====== END OUTPUT ======
        $(RM) $(LOG) $(.NULL_OUT)
        exit 1
    $(.ENDIF)
}

.TOUCH_FILE = [ common.file-touch-command ] ;
actions untar-corpus
{
    tar xf $(>) -C $(<:D)
    $(.TOUCH_FILE) $(<)
}
