#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-2-Clause
import os
import argparse
import itertools

# Makefile-fuzz-generated.am is created from this template.
MAKEFILE_FUZZ = '''# SPDX-License-Identifier: BSD-2-Clause
# Copyright (c) 2018 Intel Corporation
# All rights reserved.

if ENABLE_TCTI_FUZZING
TESTS_FUZZ = %s
%s
endif # ENABLE_TCTI_FUZZING
'''
# Each fuzz target in Makefile-fuzz-generated.am is created from this template.
MAKEFILE_FUZZ_TARGET = '''
noinst_PROGRAMS += test/fuzz/%s.fuzz
test_fuzz_%s_fuzz_CPPFLAGS = $(FUZZ_CPPFLAGS)
test_fuzz_%s_fuzz_LDADD    = $(FUZZLDADD)
nodist_test_fuzz_%s_fuzz_SOURCES  = test/fuzz/main-sys.cpp \\
        test/fuzz/%s.fuzz.cpp

DISTCLEANFILES += test/fuzz/%s.fuzz.cpp'''
# Common include definitions needed for fuzzing an SYS call
SYS_TEMPLATE_HEADER = '''/* SPDX-License-Identifier: BSD-2-Clause */
/***********************************************************************
 * Copyright (c) 2018, Intel Corporation
 *
 * All rights reserved.
 ***********************************************************************/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <stdarg.h>

#include <setjmp.h>

extern "C" {
#include "tss2_mu.h"
#include "tss2_sys.h"
#include "tss2_tcti_device.h"

#include "tss2-tcti/tcti-common.h"
#include "tss2-tcti/tcti-device.h"

#define LOGMODULE fuzz
#include "tss2_tcti.h"
#include "util/log.h"
#include "test.h"
#include "test-options.h"
#include "context-util.h"
#include "tss2-sys/sysapi_util.h"
#include "tcti/tcti-fuzzing.h"
}

extern "C"
int
test_invoke (
        TSS2_SYS_CONTEXT *sysContext)'''
# Template to call a SYS _Complete function which takes no arguments
SYS_COMPLETE_TEMPLATE_NO_ARGS = SYS_TEMPLATE_HEADER + '''
{
    %s (sysContext);

    return EXIT_SUCCESS;
}
'''
# Template to call a SYS _Complete function which takes arguments
SYS_COMPLETE_TEMPLATE_HAS_ARGS = SYS_TEMPLATE_HEADER + '''
{
    %s

    %s (
        sysContext,
        %s
    );

    return EXIT_SUCCESS;
}
'''
# Template to call a SYS _Prepare function
SYS_PREPARE_TEMPLATE_HAS_ARGS = SYS_TEMPLATE_HEADER + '''
{
    int ret;
    %s

    ret = fuzz_fill (
        sysContext,
        %d,
        %s
    );
    if (ret) {
        return ret;
    }

    %s (
        sysContext,
        %s
    );

    return EXIT_SUCCESS;
}
'''

def gen_file(function):
    '''
    Generate a cpp file used as the fuzz target given the function definition
    from a header file.
    '''
    # Parse the function name from the function definition
    function_name = function.split('\n')[0]\
                            .replace('TSS2_RC', '')\
                            .replace('(', '')\
                            .strip()
    # Parse the function arguments into an array. Do not include sysContext.
    args = [arg.strip() \
            for arg in function[function.index('(') + 1:function.index(');')]\
            .split(',') \
            if not 'TSS2_SYS_CONTEXT' in arg]
    # Prepare and Complete functions require different methods of generation.
    # Call the appropriate function to generate a cpp target specific to that
    # type of function.
    if '_Complete' in function_name:
        return gen_complete(function, function_name, args)
    if '_Prepare' in function_name:
        return gen_prepare(function, function_name, args)
    raise NotImplementedError('Unknown function type %r' % (function_name,))

def gen_complete(function, function_name, args):
    '''
    Generate the cpp fuzz target for a SYS _Complete call
    '''
    if not args:
        # Fill in the no args template. Simple case.
        return function_name, SYS_COMPLETE_TEMPLATE_NO_ARGS % (function_name)
    # Generate the cpp variable definitions.
    arg_definitions = (';\n' + ' ' * 4).join([
        arg.replace('*', '') for arg in args]) + ';'
    # Generate the cpp arguments. For arguments that are pointers find replace *
    # with & so that we pass a pointer to the definition which has been
    # allocated on the stack.
    arg_call = (',\n' + ' ' * 8).join([
        arg.replace('*', '&').split()[-1] for arg in args])
    # Fill in the template
    return function_name, SYS_COMPLETE_TEMPLATE_HAS_ARGS % (arg_definitions,
                                                             function_name,
                                                             arg_call)

def gen_prepare(function, function_name, args):
    '''
    Generate the cpp fuzz target for a SYS _Prepare call
    '''
    if not args:
        return function_name, None
    # Generate the cpp variable definitions. Make sure to initialize to empty
    # structs (works for initializing anything) or cpp compiler will complain.
    arg_definitions = (' = {0};\n' + ' ' * 4).join([
        arg.replace('*', '').replace('const', '') for arg in args]) + ' = {0};'
    # Generate the cpp arguments. For arguments that are pointers find replace *
    # with & so that we pass a pointer to the definition which has been
    # allocated on the stack.
    arg_call = (',\n' + ' ' * 8).join([
        arg.replace('*', '&').split()[-1] for arg in args])
    # Generate the call to fuzz_fill. The call should be the sysContext, double
    # the number of arguments for the _Prepare call, and then for each _Prepare
    # argument pass two to fuzz_fill, the sizeof the _Prepare argument, and a
    # pointer to it.
    fill_fuzz_args = (',\n' + ' ' * 8).join([
        ('sizeof (%s), &%s' % \
                tuple([arg.replace('*', '').split()[-1]] * 2)) \
        for arg in args])
    # Fill in the template
    return function_name, SYS_PREPARE_TEMPLATE_HAS_ARGS % (arg_definitions,
                                                            len(args) * 2,
                                                            fill_fuzz_args,
                                                            function_name,
                                                            arg_call)

def functions_from_include(header):
    '''
    Parse out and yield each function definition from a header file.
    '''
    with open(header, 'r') as header_fd:
        current_function = ''
        for line in header_fd:
            # Functions we are interested in start with _Complete or _Prepare
            if '_Complete' in line or '_Prepare' in line:
                # Set the current_function to this line
                current_function = line
            elif current_function and ');' in line:
                # When we reach the closing parenthesis yield the function
                yield current_function + line.rstrip()
                current_function = ''
            elif current_function:
                # Add all the arguments to the function
                current_function += line

def gen_files(header):
    # Generate a fuzz target cpp file from each function in the header file
    for current_function in functions_from_include(header):
        function_name, contents = gen_file(current_function)
        # Skip the yield if there is no fuzz target that can be generated
        if contents is None:
            continue
        # Yield the function name and the contents of its generated file
        yield function_name, contents

def main():
    parser = argparse.ArgumentParser(description='Generate libfuzzer for sys')
    parser.add_argument('--header', default='include/tss2/tss2_sys.h',
            help='Header file to look in (default include/tss2/tss2_sys.h)')
    args = parser.parse_args()

    functions = dict(gen_files(args.header))
    # Write the generated target to the file for its function name
    for function_name, contents in functions.items():
        filepath = os.path.join('test', 'fuzz', function_name + '.fuzz.cpp')
        with open(filepath, 'w') as fuzzer_fd:
            fuzzer_fd.write(contents)
    # Fill in the Makefile-fuzz-generated.am template using the function names.
    # Create a list of the compiled fuzz targets
    files = ' \\\n    '.join(['test/fuzz/%s.fuzz' % (function) \
                              for function in functions])
    # Create the Makefile targets for each generated file
    targets = '\n'.join([MAKEFILE_FUZZ_TARGET % tuple(list(itertools.chain(\
            ([function] * 6)))) for function in functions])
    # Write out the Makefile-fuzz-generated.am file
    with open('Makefile-fuzz-generated.am', 'w') as makefile_fd:
        makefile_fd.write(MAKEFILE_FUZZ % (files, targets))

if __name__ == '__main__':
    main()
