// Copyright 2021 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package report

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/sys/targets"
)

type DecompilerFlagMask uint64

// Extra flags that control the flow of decompilation.
const (
	FlagForceArmThumbMode DecompilerFlagMask = 1 << iota
)

const objdumpCallTimeout = 10 * time.Second

type DecompiledOpcode struct {
	Offset          int
	IsBad           bool
	Instruction     string
	FullDescription string
}

// Decompiles a byte array with opcodes into human-readable descriptions.
// Target must specify the environment from which the opcodes were taken.
func DecompileOpcodes(rawOpcodes []byte, flags DecompilerFlagMask, target *targets.Target) ([]DecompiledOpcode, error) {
	args, err := objdumpBuildArgs(flags, target)
	if err != nil {
		return nil, err
	}

	outBytes, err := objdumpExecutor(rawOpcodes, args, target)
	if err != nil {
		return nil, err
	}

	list := objdumpParseOutput(outBytes)
	if len(list) == 0 && len(rawOpcodes) > 0 {
		return nil, fmt.Errorf("no instructions found while the total size is %v bytes", len(rawOpcodes))
	}
	return list, nil
}

func objdumpExecutor(rawOpcodes []byte, args []string, target *targets.Target) ([]byte, error) {
	fileName, err := osutil.TempFile("syz-opcode-decompiler")
	if err != nil {
		return nil, fmt.Errorf("failed to create temp file: %v", err)
	}
	defer os.Remove(fileName)

	err = osutil.WriteFile(fileName, rawOpcodes)
	if err != nil {
		return nil, fmt.Errorf("failed to write to temp file: %v", err)
	}

	return osutil.RunCmd(objdumpCallTimeout, "", target.Objdump, append(args, fileName)...)
}

// nolint: lll
var objdumpAsmLineRegexp = regexp.MustCompile(`\s+([a-fA-F0-9]+)\:\s+((?:[a-fA-F0-9]{2,8}\s*)*[a-fA-F0-9]{2,8})\s+(.*?)\s*$`)

func objdumpParseOutput(rawOutput []byte) []DecompiledOpcode {
	ret := []DecompiledOpcode{}
	for s := bufio.NewScanner(bytes.NewReader(rawOutput)); s.Scan(); {
		result := objdumpAsmLineRegexp.FindStringSubmatch(string(s.Bytes()))
		if result == nil {
			continue
		}
		offset, err := strconv.ParseUint(result[1], 16, 64)
		if err != nil {
			continue
		}
		const objdumpBadInstruction = "(bad)"
		ret = append(ret, DecompiledOpcode{
			Offset:          int(offset),
			IsBad:           result[3] == objdumpBadInstruction,
			Instruction:     result[3],
			FullDescription: strings.TrimRight(result[0], " \t"),
		})
	}
	return ret
}

func objdumpBuildArgs(flags DecompilerFlagMask, target *targets.Target) ([]string, error) {
	// objdump won't be able to decompile a raw binary file unless we specify the exact
	// architecture through the -m parameter.
	ret := []string{"-b", "binary", "-D"}
	switch target.Arch {
	case targets.ARM64:
		ret = append(ret, "-maarch64")
	case targets.ARM:
		ret = append(ret, "-marm")
		if flags&FlagForceArmThumbMode != 0 {
			ret = append(ret, "-M", "force-thumb")
		}
	case targets.I386:
		ret = append(ret, "-mi386")
	case targets.AMD64:
		ret = append(ret, "-mi386", "-Mx86-64")
	case targets.MIPS64LE:
		ret = append(ret, "-mmips")
	case targets.PPC64LE:
		ret = append(ret, "-mppc")
	case targets.S390x:
		ret = append(ret, "-m", "s390:64-bit")
	case targets.RiscV64:
		ret = append(ret, "-mriscv")
	default:
		return nil, fmt.Errorf("cannot build objdump args for %#v", target.Arch)
	}

	return ret, nil
}
