// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// dumpfilter dumps the seccomp-bpf program used by the Sentry.
package main

import (
	"fmt"
	"os"

	"gvisor.dev/gvisor/pkg/bpf"
	"gvisor.dev/gvisor/pkg/log"
	"gvisor.dev/gvisor/pkg/seccomp"
	"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy/nvconf"
	"gvisor.dev/gvisor/pkg/sentry/platform/systrap"
	"gvisor.dev/gvisor/runsc/boot/filter/config"
	"gvisor.dev/gvisor/runsc/flag"
)

// Flags.
var (
	output        = flag.String("output", "fancy", "Output type: 'fancy' (human-readable with line numbers resolved), 'plain' (diffable but still human-readable output), 'bytecode' (dump raw bytecode)")
	nvproxyCaps   = flag.String("nvproxy-caps", "", "If set, enable NVProxy with the given set of NVIDIA driver capabilities")
	optimize      = flag.Bool("optimize", true, "Enable seccomp optimizations")
	denyAction    = flag.String("deny-action", "default", "What to do if the syscall matches the 'deny' ruleset (one of: errno, kill_process, kill_thread)")
	defaultAction = flag.String("default-action", "default", "What to do if all the syscall rules fail to match (one of: errno, kill_process, kill_thread)")
	badArchAction = flag.String("bad-arch-action", "default", "What to do if all the architecture field mismatches (one of: errno, kill_process, kill_thread)")
	out           = flag.String("out", "/dev/stdout", "Where to write the filter output (defaults to standard output)")
)

func action(s string) seccomp.Action {
	switch s {
	case "default":
		return seccomp.Default
	case "errno":
		return seccomp.ReturnError
	case "kill_process":
		return seccomp.KillProcess
	case "kill_thread":
		return seccomp.KillThread
	default:
		log.Warningf("invalid action %q (want one of: errno, kill_process, kill_thread)", s)
		os.Exit(1)
		panic("unreachable")
	}
}

func main() {
	flag.Parse()
	var nvCaps nvconf.DriverCaps
	if *nvproxyCaps != "" {
		flagCaps, isAll, err := nvconf.DriverCapsFromString(*nvproxyCaps)
		if err != nil {
			log.Warningf("cannot parse NVProxy capabilities: %v", err)
			os.Exit(1)
		}
		if isAll {
			flagCaps |= nvconf.AllContainerDriverCaps
		}
		nvCaps = flagCaps
	}
	opt := config.Options{
		Platform:    (&systrap.Systrap{}).SeccompInfo(),
		NVProxy:     *nvproxyCaps != "",
		NVProxyCaps: nvCaps,
	}
	rules, denyRules := config.Rules(opt)

	seccompOpts := config.SeccompOptions(opt)
	seccompOpts.SkipOptimizations = !*optimize
	seccompOpts.DefaultAction = action(*defaultAction)
	seccompOpts.BadArchAction = action(*badArchAction)
	program := &seccomp.Program{
		RuleSets: []seccomp.RuleSet{
			{
				Rules: denyRules,
			},
			{
				Rules:  rules,
				Action: seccomp.Allow,
			},
		},
		Options: seccompOpts,
	}
	insns, stats, err := program.Build()
	if err != nil {
		log.Warningf("%v", err)
		os.Exit(1)
	}
	log.Infof("Size before optimizations: %d", stats.SizeBeforeOptimizations)
	log.Infof("Size after optimizations: %d", stats.SizeAfterOptimizations)
	log.Infof("Build duration: %v", stats.BuildDuration)
	log.Infof("Rule optimization passes duration: %v", stats.RuleOptimizeDuration)
	log.Infof("BPF optimization passes duration: %v", stats.BPFOptimizeDuration)
	log.Infof("Total duration: %v", stats.BuildDuration+stats.RuleOptimizeDuration+stats.BPFOptimizeDuration)
	outFile, err := os.Create(*out)
	if err != nil {
		log.Warningf("cannot open output file %q: %v", *out, err)
		os.Exit(1)
	}
	defer outFile.Close()
	switch *output {
	case "fancy":
		dump, err := bpf.DecodeInstructions(insns)
		if err != nil {
			log.Warningf("%v", err)
			os.Exit(1)
		}
		fmt.Fprint(outFile, dump)
	case "plain":
		for _, ins := range insns {
			fmt.Fprint(outFile, ins.String())
		}
	case "bytecode":
		if _, err := outFile.WriteString(InstructionsToBytecode(insns)); err != nil {
			log.Warningf("cannot write bytecode to stdout: %v", err)
			os.Exit(1)
		}
	}
}
