// Copyright 2017 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 csource

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"

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

// Options control various aspects of source generation.
// Dashboard also provides serialized Options along with syzkaller reproducers.
type Options struct {
	Threaded    bool   `json:"threaded,omitempty"`
	Repeat      bool   `json:"repeat,omitempty"`
	RepeatTimes int    `json:"repeat_times,omitempty"` // if non-0, repeat that many times
	Procs       int    `json:"procs"`
	Slowdown    int    `json:"slowdown"`
	Sandbox     string `json:"sandbox"`
	SandboxArg  int    `json:"sandbox_arg"`

	Leak bool `json:"leak,omitempty"` // do leak checking

	// These options allow for a more fine-tuned control over the generated C code.
	NetInjection  bool `json:"tun,omitempty"`
	NetDevices    bool `json:"netdev,omitempty"`
	NetReset      bool `json:"resetnet,omitempty"`
	Cgroups       bool `json:"cgroups,omitempty"`
	BinfmtMisc    bool `json:"binfmt_misc,omitempty"`
	CloseFDs      bool `json:"close_fds"`
	KCSAN         bool `json:"kcsan,omitempty"`
	DevlinkPCI    bool `json:"devlinkpci,omitempty"`
	NicVF         bool `json:"nicvf,omitempty"`
	USB           bool `json:"usb,omitempty"`
	VhciInjection bool `json:"vhci,omitempty"`
	Wifi          bool `json:"wifi,omitempty"`
	IEEE802154    bool `json:"ieee802154,omitempty"`
	Sysctl        bool `json:"sysctl,omitempty"`
	Swap          bool `json:"swap,omitempty"`

	UseTmpDir  bool `json:"tmpdir,omitempty"`
	HandleSegv bool `json:"segv,omitempty"`

	// Generate code for use with repro package to prints log messages,
	// which allows to detect hangs.
	Repro bool `json:"repro,omitempty"`
	Trace bool `json:"trace,omitempty"`
	LegacyOptions
}

// These are legacy options, they remain only for the sake of backward compatibility.
type LegacyOptions struct {
	Collide   bool `json:"collide,omitempty"`
	Fault     bool `json:"fault,omitempty"`
	FaultCall int  `json:"fault_call,omitempty"`
	FaultNth  int  `json:"fault_nth,omitempty"`
}

// Check checks if the opts combination is valid or not.
// For example, Collide without Threaded is not valid.
// Invalid combinations must not be passed to Write.
func (opts Options) Check(OS string) error {
	switch opts.Sandbox {
	case "", sandboxNone, sandboxNamespace, sandboxSetuid, sandboxAndroid:
	default:
		return fmt.Errorf("unknown sandbox %v", opts.Sandbox)
	}
	if !opts.Threaded && opts.Collide {
		// Collide requires threaded.
		return errors.New("option Collide without Threaded")
	}
	if !opts.Repeat {
		if opts.Procs > 1 {
			// This does not affect generated code.
			return errors.New("option Procs>1 without Repeat")
		}
		if opts.NetReset {
			return errors.New("option NetReset without Repeat")
		}
		if opts.RepeatTimes > 1 {
			return errors.New("option RepeatTimes without Repeat")
		}
	}
	if opts.Sandbox == "" {
		if opts.NetInjection {
			return errors.New("option NetInjection without sandbox")
		}
		if opts.NetDevices {
			return errors.New("option NetDevices without sandbox")
		}
		if opts.Cgroups {
			return errors.New("option Cgroups without sandbox")
		}
		if opts.BinfmtMisc {
			return errors.New("option BinfmtMisc without sandbox")
		}
		if opts.VhciInjection {
			return errors.New("option VhciInjection without sandbox")
		}
		if opts.Wifi {
			return errors.New("option Wifi without sandbox")
		}
	}
	if opts.Sandbox == sandboxNamespace && !opts.UseTmpDir {
		// This is borken and never worked.
		// This tries to create syz-tmp dir in cwd,
		// which will fail if procs>1 and on second run of the program.
		return errors.New("option Sandbox=namespace without UseTmpDir")
	}
	if opts.NetReset && (opts.Sandbox == "" || opts.Sandbox == sandboxSetuid) {
		return errors.New("option NetReset without sandbox")
	}
	if opts.Cgroups && !opts.UseTmpDir {
		return errors.New("option Cgroups without UseTmpDir")
	}
	return opts.checkLinuxOnly(OS)
}

func (opts Options) checkLinuxOnly(OS string) error {
	if OS == targets.Linux {
		return nil
	}
	if opts.NetInjection && !(OS == targets.OpenBSD || OS == targets.FreeBSD || OS == targets.NetBSD) {
		return fmt.Errorf("option NetInjection is not supported on %v", OS)
	}
	if opts.Sandbox == sandboxNamespace ||
		(opts.Sandbox == sandboxSetuid && !(OS == targets.OpenBSD || OS == targets.FreeBSD || OS == targets.NetBSD)) ||
		opts.Sandbox == sandboxAndroid {
		return fmt.Errorf("option Sandbox=%v is not supported on %v", opts.Sandbox, OS)
	}
	for name, opt := range map[string]*bool{
		"NetDevices":    &opts.NetDevices,
		"NetReset":      &opts.NetReset,
		"Cgroups":       &opts.Cgroups,
		"BinfmtMisc":    &opts.BinfmtMisc,
		"CloseFDs":      &opts.CloseFDs,
		"KCSAN":         &opts.KCSAN,
		"DevlinkPCI":    &opts.DevlinkPCI,
		"NicVF":         &opts.NicVF,
		"USB":           &opts.USB,
		"VhciInjection": &opts.VhciInjection,
		"Wifi":          &opts.Wifi,
		"ieee802154":    &opts.IEEE802154,
		"Fault":         &opts.Fault,
		"Leak":          &opts.Leak,
		"Sysctl":        &opts.Sysctl,
		"Swap":          &opts.Swap,
	} {
		if *opt {
			return fmt.Errorf("option %v is not supported on %v", name, OS)
		}
	}
	return nil
}

func DefaultOpts(cfg *mgrconfig.Config) Options {
	opts := Options{
		Threaded:   true,
		Repeat:     true,
		Procs:      cfg.Procs,
		Slowdown:   cfg.Timeouts.Slowdown,
		Sandbox:    cfg.Sandbox,
		UseTmpDir:  true,
		HandleSegv: true,
		Repro:      true,
	}
	if cfg.TargetOS == targets.Linux {
		opts.NetInjection = true
		opts.NetDevices = true
		opts.NetReset = true
		opts.Cgroups = true
		opts.BinfmtMisc = true
		opts.CloseFDs = true
		opts.DevlinkPCI = true
		opts.NicVF = true
		opts.USB = true
		opts.VhciInjection = true
		opts.Wifi = true
		opts.IEEE802154 = true
		opts.Sysctl = true
		opts.Swap = true
	}
	if cfg.Sandbox == "" || cfg.Sandbox == "setuid" {
		opts.NetReset = false
	}
	if err := opts.Check(cfg.TargetOS); err != nil {
		panic(fmt.Sprintf("DefaultOpts created bad opts: %v", err))
	}
	return opts
}

func (opts Options) Serialize() []byte {
	data, err := json.Marshal(opts)
	if err != nil {
		panic(err)
	}
	return data
}

func deserializeLegacyOptions1(data string, opts *Options) error {
	waitRepeat, debug := false, false
	n, err := fmt.Sscanf(data,
		"{Threaded:%t Collide:%t Repeat:%t Procs:%d Sandbox:%s"+
			" Fault:%t FaultCall:%d FaultNth:%d EnableTun:%t UseTmpDir:%t"+
			" HandleSegv:%t WaitRepeat:%t Debug:%t Repro:%t}",
		&opts.Threaded, &opts.Collide, &opts.Repeat, &opts.Procs, &opts.Sandbox,
		&opts.Fault, &opts.FaultCall, &opts.FaultNth, &opts.NetInjection, &opts.UseTmpDir,
		&opts.HandleSegv, &waitRepeat, &debug, &opts.Repro)

	if err == nil {
		if want := 14; n != want {
			return fmt.Errorf("failed to parse repro options: got %v fields, want %v", n, want)
		}
	}
	return err
}

func deserializeLegacyOptions2(data string, opts *Options) error {
	waitRepeat, debug := false, false
	n, err := fmt.Sscanf(data,
		"{Threaded:%t Collide:%t Repeat:%t Procs:%d Sandbox:%s"+
			" Fault:%t FaultCall:%d FaultNth:%d EnableTun:%t UseTmpDir:%t"+
			" EnableCgroups:%t HandleSegv:%t WaitRepeat:%t Debug:%t Repro:%t}",
		&opts.Threaded, &opts.Collide, &opts.Repeat, &opts.Procs, &opts.Sandbox,
		&opts.Fault, &opts.FaultCall, &opts.FaultNth, &opts.NetInjection, &opts.UseTmpDir,
		&opts.Cgroups, &opts.HandleSegv, &waitRepeat, &debug, &opts.Repro)
	if err == nil {
		if want := 15; n != want {
			return fmt.Errorf("failed to parse repro options 2: got %v fields, want %v", n, want)
		}
	}
	return err
}

// Android format.
func deserializeLegacyOptions3(data string, opts *Options) error {
	waitRepeat, debug := false, false
	n, err := fmt.Sscanf(data,
		"{Threaded:%t Collide:%t Repeat:%t Procs:%d Sandbox:%s SandboxArg:%d"+
			" Fault:%t FaultCall:%d FaultNth:%d EnableTun:%t UseTmpDir:%t"+
			" EnableCgroups:%t HandleSegv:%t WaitRepeat:%t Debug:%t Repro:%t}",
		&opts.Threaded, &opts.Collide, &opts.Repeat, &opts.Procs, &opts.Sandbox, &opts.SandboxArg,
		&opts.Fault, &opts.FaultCall, &opts.FaultNth, &opts.NetInjection, &opts.UseTmpDir,
		&opts.Cgroups, &opts.HandleSegv, &waitRepeat, &debug, &opts.Repro)
	if err == nil {
		if want := 16; n != want {
			return fmt.Errorf("failed to parse repro options 3: got %v fields, want %v", n, want)
		}
	}
	return err
}

var parsers = []func(string, *Options) error{
	deserializeLegacyOptions1,
	deserializeLegacyOptions2,
	deserializeLegacyOptions3,
}

// Support for legacy formats.
func deserializeLegacyFormats(data []byte, opts *Options) error {
	data = bytes.Replace(data, []byte("Sandbox: "), []byte("Sandbox:empty "), -1)
	strData := string(data)

	// We can distinguish between legacy formats by the number
	// of fields. The formats we support have 14, 15 and 16.
	// Each field can be recognized by ':' char.
	version := strings.Count(strData, ":")
	if version < 14 || version > 16 {
		return fmt.Errorf("unrecognized options format")
	}
	index := version - 14
	err := parsers[index](strData, opts)

	if opts.Sandbox == "empty" {
		opts.Sandbox = ""
	}
	return err
}

func DeserializeOptions(data []byte) (Options, error) {
	opts := Options{
		Slowdown: 1,
		// Before CloseFDs was added, close_fds() was always called, so default to true.
		CloseFDs: true,
	}
	if err := json.Unmarshal(data, &opts); err == nil {
		return opts, nil
	}
	err := deserializeLegacyFormats(data, &opts)
	return opts, err
}

type Feature struct {
	Description string
	Enabled     bool
}

type Features map[string]Feature

func defaultFeatures(value bool) Features {
	return map[string]Feature{
		"tun":         {"setup and use /dev/tun for packet injection", value},
		"net_dev":     {"setup more network devices for testing", value},
		"net_reset":   {"reset network namespace between programs", value},
		"cgroups":     {"setup cgroups for testing", value},
		"binfmt_misc": {"setup binfmt_misc for testing", value},
		"close_fds":   {"close fds after each program", value},
		"devlink_pci": {"setup devlink PCI device", value},
		"nic_vf":      {"setup NIC VF device", value},
		"usb":         {"setup and use /dev/raw-gadget for USB emulation", value},
		"vhci":        {"setup and use /dev/vhci for hci packet injection", value},
		"wifi":        {"setup and use mac80211_hwsim for wifi emulation", value},
		"ieee802154":  {"setup and use mac802154_hwsim for emulation", value},
		"sysctl":      {"setup sysctl's for fuzzing", value},
		"swap":        {"setup and use a swap file", value},
	}
}

func ParseFeaturesFlags(enable, disable string, defaultValue bool) (Features, error) {
	const (
		none = "none"
		all  = "all"
	)
	if enable == none && disable == none {
		return defaultFeatures(defaultValue), nil
	}
	if enable != none && disable != none {
		return nil, fmt.Errorf("can't use -enable and -disable flags at the same time")
	}
	if enable == all || disable == "" {
		return defaultFeatures(true), nil
	}
	if disable == all || enable == "" {
		return defaultFeatures(false), nil
	}
	var items []string
	var features Features
	if enable != none {
		items = strings.Split(enable, ",")
		features = defaultFeatures(false)
	} else {
		items = strings.Split(disable, ",")
		features = defaultFeatures(true)
	}
	for _, item := range items {
		if _, ok := features[item]; !ok {
			return nil, fmt.Errorf("unknown feature specified: %s", item)
		}
		feature := features[item]
		feature.Enabled = enable != none
		features[item] = feature
	}
	return features, nil
}

func PrintAvailableFeaturesFlags() {
	fmt.Printf("available features for -enable and -disable:\n")
	features := defaultFeatures(false)
	var names []string
	for name := range features {
		names = append(names, name)
	}
	sort.Strings(names)
	for _, name := range names {
		fmt.Printf("  %s - %s\n", name, features[name].Description)
	}
}

// This is the main configuration used by executor, only for testing.
var ExecutorOpts = Options{
	Threaded:  true,
	Repeat:    true,
	Procs:     2,
	Slowdown:  1,
	Sandbox:   "none",
	Repro:     true,
	UseTmpDir: true,
}
