// Copyright 2021 The Android Open Source Project
//
// 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.

package selinux

import (
	"github.com/google/blueprint/proptools"

	"fmt"
	"strconv"

	"android/soong/android"
)

func init() {
	ctx := android.InitRegistrationContext
	ctx.RegisterModuleType("se_neverallow_test", neverallowTestFactory)
}

type neverallowTestProperties struct {
	// Default modules for conf
	Defaults []string

	// Policy files to be tested.
	Srcs []string `android:"path"`
}

type neverallowTestModule struct {
	android.ModuleBase
	properties    neverallowTestProperties
	testTimestamp android.OutputPath
}

type nameProperties struct {
	Name *string
}

var checkpolicyTag = dependencyTag{name: "checkpolicy"}
var sepolicyAnalyzeTag = dependencyTag{name: "sepolicy_analyze"}

// se_neverallow_test builds given policy files and checks whether any neverallow violations exist.
// This module creates two conf files, one with build test and one without build test. Policy with
// build test will be compiled with checkpolicy, and policy without build test will be tested with
// sepolicy-analyze's neverallow tool.  This module's check can be skipped by setting
// SELINUX_IGNORE_NEVERALLOWS := true.
func neverallowTestFactory() android.Module {
	n := &neverallowTestModule{}
	n.AddProperties(&n.properties)
	android.InitAndroidArchModule(n, android.DeviceSupported, android.MultilibCommon)
	android.AddLoadHook(n, func(ctx android.LoadHookContext) {
		n.loadHook(ctx)
	})
	return n
}

// Child conf module name for checkpolicy test.
func (n *neverallowTestModule) checkpolicyConfModuleName() string {
	return n.Name() + ".checkpolicy.conf"
}

// Child conf module name for sepolicy-analyze test.
func (n *neverallowTestModule) sepolicyAnalyzeConfModuleName() string {
	return n.Name() + ".sepolicy_analyze.conf"
}

func (n *neverallowTestModule) loadHook(ctx android.LoadHookContext) {
	checkpolicyConf := n.checkpolicyConfModuleName()
	ctx.CreateModule(policyConfFactory, &nameProperties{
		Name: proptools.StringPtr(checkpolicyConf),
	}, &policyConfProperties{
		Srcs:          n.properties.Srcs,
		Build_variant: proptools.StringPtr("user"),
		Installable:   proptools.BoolPtr(false),
	}, &struct {
		Defaults []string
	}{
		Defaults: n.properties.Defaults,
	})

	sepolicyAnalyzeConf := n.sepolicyAnalyzeConfModuleName()
	ctx.CreateModule(policyConfFactory, &nameProperties{
		Name: proptools.StringPtr(sepolicyAnalyzeConf),
	}, &policyConfProperties{
		Srcs:               n.properties.Srcs,
		Build_variant:      proptools.StringPtr("user"),
		Exclude_build_test: proptools.BoolPtr(true),
		Installable:        proptools.BoolPtr(false),
	}, &struct {
		Defaults []string
	}{
		Defaults: n.properties.Defaults,
	})
}

func (n *neverallowTestModule) DepsMutator(ctx android.BottomUpMutatorContext) {
	ctx.AddDependency(n, checkpolicyTag, n.checkpolicyConfModuleName())
	ctx.AddDependency(n, sepolicyAnalyzeTag, n.sepolicyAnalyzeConfModuleName())
}

func (n *neverallowTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	n.testTimestamp = pathForModuleOut(ctx, "timestamp")
	if ctx.Config().SelinuxIgnoreNeverallows() {
		// just touch
		android.WriteFileRule(ctx, n.testTimestamp, "")
		return
	}

	var checkpolicyConfPaths android.Paths
	var sepolicyAnalyzeConfPaths android.Paths

	ctx.VisitDirectDepsProxy(func(child android.ModuleProxy) {
		depTag := ctx.OtherModuleDependencyTag(child)
		if depTag != checkpolicyTag && depTag != sepolicyAnalyzeTag {
			return
		}

		outputs := android.OutputFilesForModule(ctx, child, "")

		switch ctx.OtherModuleDependencyTag(child) {
		case checkpolicyTag:
			checkpolicyConfPaths = outputs
		case sepolicyAnalyzeTag:
			sepolicyAnalyzeConfPaths = outputs
		}
	})

	if len(checkpolicyConfPaths) != 1 {
		panic(fmt.Errorf("Module %q should produce exactly one output", n.checkpolicyConfModuleName()))
	}

	if len(sepolicyAnalyzeConfPaths) != 1 {
		panic(fmt.Errorf("Module %q should produce exactly one output", n.sepolicyAnalyzeConfModuleName()))
	}

	checkpolicyConfPath := checkpolicyConfPaths[0]
	sepolicyAnalyzeConfPath := sepolicyAnalyzeConfPaths[0]

	rule := android.NewRuleBuilder(pctx, ctx)

	// Step 1. Build a binary policy from the conf file including build test
	binaryPolicy := pathForModuleOut(ctx, "policy")
	rule.Command().BuiltTool("checkpolicy").
		Flag("-M").
		FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
		FlagWithOutput("-o ", binaryPolicy).
		Input(checkpolicyConfPath)
	rule.Build("neverallow_checkpolicy", "Neverallow check: "+ctx.ModuleName())

	// Step 2. Run sepolicy-analyze with the conf file without the build test and binary policy
	// file from Step 1
	rule = android.NewRuleBuilder(pctx, ctx)
	msg := `sepolicy-analyze failed. This is most likely due to the use\n` +
		`of an expanded attribute in a neverallow assertion. Please fix\n` +
		`the policy.`

	rule.Command().BuiltTool("sepolicy-analyze").
		Input(binaryPolicy).
		Text("neverallow").
		Flag("-w").
		FlagWithInput("-f ", sepolicyAnalyzeConfPath).
		Text("|| (echo").
		Flag("-e").
		Text(`"` + msg + `"`).
		Text("; exit 1)")

	rule.Command().Text("touch").Output(n.testTimestamp)
	rule.Build("neverallow_sepolicy-analyze", "Neverallow check: "+ctx.ModuleName())

	ctx.CheckbuildFile(n.testTimestamp)

	moduleInfoJSON := ctx.ModuleInfoJSON()
	moduleInfoJSON.Class = []string{"FAKE"}
	moduleInfoJSON.SystemSharedLibs = []string{"none"}
}

func (n *neverallowTestModule) AndroidMkEntries() []android.AndroidMkEntries {
	return []android.AndroidMkEntries{android.AndroidMkEntries{
		OutputFile: android.OptionalPathForPath(n.testTimestamp),
		Class:      "FAKE",
		Include:    "$(BUILD_PHONY_PACKAGE)",
		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
				entries.SetPath("LOCAL_ADDITIONAL_DEPENDENCIES", n.testTimestamp)
			},
		},
	}}
}
