#!/usr/bin/env python3
# 
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

import argparse
import os
import sys
import ast
import pickle
import unicorn

from qiling import Qiling
from qiling.arch import utils as arch_utils
from qiling.debugger.qdb import QlQdb
from qiling.utils import arch_convert
from qiling.const import QL_VERBOSE, QL_ENDIAN, os_map, arch_map, verbose_map
from qiling.__version__ import __version__ as ql_version
from qiling.extensions.coverage import utils as cov_utils
from qiling.extensions import report

# read code from file
def read_file(fname: str):
    with open(fname, "rb") as f:
        content = f.read()

    return content

class __arg_env(argparse.Action):
    def __call__(self, parser, namespace, values, option_string):
        env = {}

        if os.path.exists(values):
            with open(values, 'rb') as f:
                env = pickle.load(f)
        else:
            env = ast.literal_eval(values)

        setattr(namespace, self.dest, env)

class __arg_verbose(argparse.Action):
    def __call__(self, parser, namespace, values, option_string):
        setattr(namespace, self.dest, verbose_map[values])

def handle_code(options: argparse.Namespace):
    if options.format == 'hex':
        if options.input is not None:
            print ("Load HEX from ARGV")
            code = str(options.input).strip("\\\\x").split("x")
            code = "".join(code).strip()
            code =  bytes.fromhex(code)
        elif options.filename is not None:
            print ("Load HEX from FILE")
            code = str(read_file(options.filename)).strip('b\'').strip('\\n')
            code = code.strip('x').split("\\\\x")
            code = "".join(code).strip()
            code = bytes.fromhex(code)
        else:
            print("ERROR: File not found")
            exit(1)

    elif options.format == 'asm':
        print ("Load ASM from FILE")
        assembly = read_file(options.filename)
        archtype = arch_convert(options.arch)

        archendian = {
            'little': QL_ENDIAN.EL,
            'big'   : QL_ENDIAN.EB
        }[options.endian]

        assembler = arch_utils.assembler(archtype, archendian)
        code, _ = assembler.asm(assembly)
        code = bytes(code)

    elif options.format == 'bin':
        print ("Load BIN from FILE")
        if options.filename is not None:
            code = read_file(options.filename)
        else:
            print("ERROR: File not found")
            exit(1)

    ql = Qiling(
        rootfs=options.rootfs,
        env=options.env,
        code=code,
        ostype=options.os,
        archtype=options.arch,
        bigendian=(options.endian == 'big'),
        verbose=options.verbose,
        profile=options.profile,
        filter=options.filter
    )

    return ql

def handle_run(options: argparse.Namespace):
    effective_argv = []

    # with argv
    if options.filename is not None and options.run_args == []:
        effective_argv = [options.filename] + options.args

    # Without argv
    elif options.filename is None and options.args == [] and options.run_args != []:
        effective_argv = options.run_args

    else:
        print("ERROR: Command error!")

    ql = Qiling(
        argv=effective_argv,
        rootfs=options.rootfs,
        env=options.env,
        verbose=options.verbose,
        profile=options.profile,
        console=options.console,
        log_file=options.log_file,
        log_plain=options.log_plain,
        multithread=options.multithread,
        filter=options.filter
    )

    # attach Qdb at entry point
    if options.qdb is True:
        QlQdb(ql, rr=options.rr).run()
        exit()

    return ql

def handle_examples(parser: argparse.ArgumentParser):
    prog = os.path.basename(__file__)

    __ql_examples = f"""Examples:

    With code:
        {prog} code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex
        {prog} code --os linux --arch x86 --format asm -f examples/shellcodes/lin32_execve.asm

    With binary file:
        {prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux
        {prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux

    With binary file and Qdb:
        {prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb
        {prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb --rr

    With binary file and gdbserver:
        {prog} run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux

    With binary file and additional argv:
        {prog} run -f examples/rootfs/x8664_linux/bin/x8664_args --rootfs examples/rootfs/x8664_linux --args test1 test2 test3

    With binary file and various output format:
        {prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --verbose disasm
        {prog} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --filter ^open

    With UEFI file:
        {prog} run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --env examples/rootfs/x8664_efi/rom2_nvar.pickel

    With binary file and json output:
        {prog} run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows --no-console --json

"""

    parser.exit(0, __ql_examples)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--version', action='version', version=f'qltool for Qiling {ql_version}, using Unicorn {unicorn.__version__}')

    commands = parser.add_subparsers(title='sub commands', description='select execution mode', dest='subcommand')

    # set "run" subcommand options
    run_parser = commands.add_parser('run', help='run a program')
    run_parser.add_argument('-f', '--filename', default=None, metavar="FILE", help="filename")
    run_parser.add_argument('--rootfs', required=True, help='emulated rootfs')
    run_parser.add_argument('--args', default=[], nargs=argparse.REMAINDER, dest="args", help="args")
    run_parser.add_argument('run_args', default=[], nargs=argparse.REMAINDER)

    # set "code" subcommand options
    code_parser = commands.add_parser('code', help='execute a shellcode')
    code_parser.add_argument('-f', '--filename', metavar="FILE", help="filename")
    code_parser.add_argument('-i', '--input', metavar="INPUT", dest="input", help='input hex value')
    code_parser.add_argument('--arch', required=True, choices=arch_map)
    code_parser.add_argument('--endian', choices=('little', 'big'), default='little')
    code_parser.add_argument('--os', required=True, choices=os_map)
    code_parser.add_argument('--rootfs', help='emulated root filesystem, that is where all libraries reside')
    code_parser.add_argument('--format', choices=('asm', 'hex', 'bin'), default='bin', help='input file format')

    # set "examples" subcommand
    expl_parser = commands.add_parser('examples', help='show examples and exit', add_help=False)

    comm_parser = run_parser

    if len(sys.argv) > 1 and sys.argv[1] == 'code':
        comm_parser = code_parser

    # set common options
    comm_parser.add_argument('-v', '--verbose', choices=verbose_map, default=QL_VERBOSE.DEFAULT, action=__arg_verbose, help='set verbosity level')
    comm_parser.add_argument('--env', metavar="FILE", action=__arg_env, help="pickle file containing an environment dictionary")
    comm_parser.add_argument('-g', '--gdb', nargs='?', metavar='SERVER:PORT', const='gdb', help='enable gdb server')
    comm_parser.add_argument('--qdb', action='store_true', help='attach Qdb at entry point, it\'s MIPS, ARM(THUMB) supported only for now')
    comm_parser.add_argument('--rr', action='store_true', help='switch on record and replay feature in qdb, only works with --qdb')
    comm_parser.add_argument('--profile', help="define a customized profile")
    comm_parser.add_argument('--no-console', action='store_false', dest='console', help='do not emit output to console')
    comm_parser.add_argument('-e', '--filter', metavar='REGEXP', default=None, help="apply a filtering regexp on log output")
    comm_parser.add_argument('--log-file', help="write log to a file")
    comm_parser.add_argument('--log-plain', action='store_true', help="do not use colors in log output")
    comm_parser.add_argument('--root', action='store_true', help='enable sudo required mode')
    comm_parser.add_argument('--debug-stop', action='store_true', help='stop running on error; requires verbose to be set to either "debug" or "dump"')
    comm_parser.add_argument('-m', '--multithread',action='store_true', help='run in multithread mode')
    comm_parser.add_argument('--timeout', type=int, default=0, help='set emulation timeout')
    comm_parser.add_argument('-c', '--coverage-file', default=None, help='code coverage file name')
    comm_parser.add_argument('--coverage-format', default='drcov', choices=cov_utils.factory.formats, help='code coverage file format')
    comm_parser.add_argument('--json', action='store_true', help='print a json report of the emulation')
    options = parser.parse_args()

    # subparser argument required=True is not supported in python 3.6
    # manually check whether the user did not specify a subcommand (execution mode)
    if not options.subcommand:
        parser.error('please select execution mode')

    if options.subcommand == 'examples':
        handle_examples(parser)

    if options.debug_stop and options.verbose not in (QL_VERBOSE.DEBUG, QL_VERBOSE.DUMP):
        parser.error('the debug_stop option requires verbose to be set to either "debug" or "dump"')

    # ql file setup
    if options.subcommand == 'run':
        ql = handle_run(options)

    # ql code setup
    elif options.subcommand == 'code':
        ql = handle_code(options)

    # ql execute additional options
    if options.gdb:
        argval = options.gdb

        if argval != 'gdb':
            argval = f'gdb:{argval}'

        ql.debugger = argval

    if options.debug_stop:
        ql.debug_stop = True

    if options.root:
        ql.root = True

    # ql run
    with cov_utils.collect_coverage(ql, options.coverage_format, options.coverage_file):
        ql.run(timeout=options.timeout)

    if options.json:
        print(report.generate_report(ql, pretty_print=True))

    exit(ql.os.exit_code)
