// Copyright (C) 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 (
	"os"
	"sort"
	"strconv"
	"strings"

	"github.com/google/blueprint/proptools"

	"android/soong/android"
)

const (
	MlsSens    = 1
	MlsCats    = 1024
	PolicyVers = 30
)

// This order should be kept. checkpolicy syntax requires it.
var policyConfOrder = []string{
	"flagging_macros",
	"security_classes",
	"initial_sids",
	"access_vectors",
	"global_macros",
	"neverallow_macros",
	"mls_macros",
	"mls_decl",
	"mls",
	"policy_capabilities",
	"te_macros",
	"ioctl_defines",
	"ioctl_macros",
	"nlmsg_defines",
	"nlmsg_macros",
	"attributes|*.te",
	"roles_decl",
	"roles",
	"users",
	"initial_sid_contexts",
	"fs_use",
	"genfs_contexts",
	"port_contexts",
}

func init() {
	android.RegisterModuleType("se_policy_conf", policyConfFactory)
	android.RegisterModuleType("se_policy_conf_defaults", policyConfDefaultFactory)
	android.RegisterModuleType("se_policy_cil", policyCilFactory)
	android.RegisterModuleType("se_policy_binary", policyBinaryFactory)
}

type policyConfProperties struct {
	// Name of the output. Default is {module_name}
	Stem *string

	// Policy files to be compiled to cil file.
	Srcs []string `android:"path"`

	// Target build variant (user / userdebug / eng). Default follows the current lunch target
	Build_variant *string

	// Whether to exclude build test or not. Default is false
	Exclude_build_test *bool

	// Whether to include asan specific policies or not. Default follows the current lunch target
	With_asan *bool

	// Whether to build CTS specific policy or not. Default is false
	Cts *bool

	// Whether to build recovery specific policy or not. Default is false
	Target_recovery *bool

	// Whether this module is directly installable to one of the partitions. Default is true
	Installable *bool

	// Desired number of MLS categories. Defaults to 1024
	Mls_cats *int64

	// Board api level of policy files. Set "current" for RELEASE_BOARD_API_LEVEL, or a direct
	// version string (e.g. "202404"). Defaults to "current"
	Board_api_level *string

	// Leave only neverallow rules and line markers. This minimizes the output conf files used for
	// neverallow CTS tests. Default is false
	Only_neverallow_rules *bool
}

type policyConf struct {
	android.ModuleBase
	android.DefaultableModuleBase
	flaggableModuleBase

	properties policyConfProperties

	installSource android.Path
	installPath   android.InstallPath
}

var _ flaggableModule = (*policyConf)(nil)

// se_policy_conf merges collection of policy files into a policy.conf file to be processed by
// checkpolicy.
func policyConfFactory() android.Module {
	c := &policyConf{}
	c.AddProperties(&c.properties)
	initFlaggableModule(c)
	android.InitAndroidArchModule(c, android.DeviceSupported, android.MultilibCommon)
	android.InitDefaultableModule(c)
	return c
}

type policyConfDefaults struct {
	android.ModuleBase
	android.DefaultsModuleBase
}

// se_policy_conf_defaults provides a set of properties that can be inherited by other
// se_policy_conf_defaults modules. A module can use the properties from a se_policy_conf_defaults
// using `defaults: ["<:default_module_name>"]`. Properties of both modules are merged (when
// possible) by prepending the default module's values to the depending module's values.
func policyConfDefaultFactory() android.Module {
	c := &policyConfDefaults{}
	c.AddProperties(
		&policyConfProperties{},
		&flaggableModuleProperties{},
	)
	android.InitDefaultsModule(c)
	return c
}

func (c *policyConf) installable() bool {
	return proptools.BoolDefault(c.properties.Installable, true)
}

func (c *policyConf) stem() string {
	return proptools.StringDefault(c.properties.Stem, c.Name())
}

func (c *policyConf) buildVariant(ctx android.ModuleContext) string {
	if variant := proptools.String(c.properties.Build_variant); variant != "" {
		return variant
	}
	if ctx.Config().Eng() {
		return "eng"
	}
	if ctx.Config().Debuggable() {
		return "userdebug"
	}
	return "user"
}

func (c *policyConf) cts() bool {
	return proptools.Bool(c.properties.Cts)
}

func (c *policyConf) isTargetRecovery() bool {
	return proptools.Bool(c.properties.Target_recovery)
}

func (c *policyConf) withAsan(ctx android.ModuleContext) string {
	isAsanDevice := android.InList("address", ctx.Config().SanitizeDevice())
	return strconv.FormatBool(proptools.BoolDefault(c.properties.With_asan, isAsanDevice))
}

func (c *policyConf) sepolicySplit(ctx android.ModuleContext) string {
	if c.cts() {
		return "cts"
	}
	if c.isTargetRecovery() {
		return "false"
	}
	return strconv.FormatBool(true)
}

func (c *policyConf) compatibleProperty(ctx android.ModuleContext) string {
	if c.cts() {
		return "cts"
	}
	if c.isTargetRecovery() {
		return "false"
	}
	return "true"
}

func (c *policyConf) trebleSyspropNeverallow(ctx android.ModuleContext) string {
	if c.cts() {
		return "cts"
	}
	if c.isTargetRecovery() {
		return "false"
	}
	return strconv.FormatBool(!ctx.DeviceConfig().BuildBrokenTrebleSyspropNeverallow())
}

func (c *policyConf) enforceSyspropOwner(ctx android.ModuleContext) string {
	if c.cts() {
		return "cts"
	}
	if c.isTargetRecovery() {
		return "false"
	}
	return strconv.FormatBool(!ctx.DeviceConfig().BuildBrokenEnforceSyspropOwner())
}

func (c *policyConf) enforceDebugfsRestrictions(ctx android.ModuleContext) string {
	if c.cts() {
		return "cts"
	}
	return strconv.FormatBool(ctx.DeviceConfig().BuildDebugfsRestrictionsEnabled())
}

func (c *policyConf) mlsCats() int {
	return proptools.IntDefault(c.properties.Mls_cats, MlsCats)
}

func findPolicyConfOrder(name string) int {
	for idx, pattern := range policyConfOrder {
		// We could use regexp but it seems like an overkill
		if pattern == "attributes|*.te" && (name == "attributes" || strings.HasSuffix(name, ".te")) {
			return idx
		} else if pattern == name {
			return idx
		}
	}
	// name is not matched
	return len(policyConfOrder)
}

func (c *policyConf) transformPolicyToConf(ctx android.ModuleContext) android.OutputPath {
	conf := pathForModuleOut(ctx, c.stem())
	rule := android.NewRuleBuilder(pctx, ctx)

	srcs := android.PathsForModuleSrc(ctx, c.properties.Srcs)
	sort.SliceStable(srcs, func(x, y int) bool {
		return findPolicyConfOrder(srcs[x].Base()) < findPolicyConfOrder(srcs[y].Base())
	})

	flags := c.getBuildFlags(ctx)
	rule.Command().Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
		Flag("--fatal-warnings").
		FlagForEachArg("-D ", ctx.DeviceConfig().SepolicyM4Defs()).
		FlagWithArg("-D mls_num_sens=", strconv.Itoa(MlsSens)).
		FlagWithArg("-D mls_num_cats=", strconv.Itoa(c.mlsCats())).
		FlagWithArg("-D target_arch=", ctx.DeviceConfig().DeviceArch()).
		FlagWithArg("-D target_with_asan=", c.withAsan(ctx)).
		FlagWithArg("-D target_with_dexpreopt=", strconv.FormatBool(ctx.DeviceConfig().WithDexpreopt())).
		FlagWithArg("-D target_with_native_coverage=", strconv.FormatBool(ctx.DeviceConfig().ClangCoverageEnabled() || ctx.DeviceConfig().GcovCoverageEnabled())).
		FlagWithArg("-D target_build_variant=", c.buildVariant(ctx)).
		FlagWithArg("-D target_full_treble=", c.sepolicySplit(ctx)).
		FlagWithArg("-D target_compatible_property=", c.compatibleProperty(ctx)).
		FlagWithArg("-D target_treble_sysprop_neverallow=", c.trebleSyspropNeverallow(ctx)).
		FlagWithArg("-D target_enforce_sysprop_owner=", c.enforceSyspropOwner(ctx)).
		FlagWithArg("-D target_exclude_build_test=", strconv.FormatBool(proptools.Bool(c.properties.Exclude_build_test))).
		FlagWithArg("-D target_requires_insecure_execmem_for_swiftshader=", strconv.FormatBool(ctx.DeviceConfig().RequiresInsecureExecmemForSwiftshader())).
		FlagWithArg("-D target_enforce_debugfs_restriction=", c.enforceDebugfsRestrictions(ctx)).
		FlagWithArg("-D target_recovery=", strconv.FormatBool(c.isTargetRecovery())).
		Flag(boardApiLevelToM4Macro(ctx, c.properties.Board_api_level)).
		Flags(flagsToM4Macros(flags)).
		Flag("-s").
		Inputs(srcs).
		Text("> ").Output(conf)

	if proptools.Bool(c.properties.Only_neverallow_rules) {
		rule.Command().BuiltTool("sepolicy_filter_neverallow").
			Text(conf.String()). // input
			Text(conf.String())  // output (in-place filtering)
	}

	rule.Build("conf", "Transform policy to conf: "+ctx.ModuleName())
	return conf
}

func (c *policyConf) DepsMutator(ctx android.BottomUpMutatorContext) {
	c.flagDeps(ctx)
}

func (c *policyConf) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	if !c.installable() {
		c.SkipInstall()
	}

	c.installSource = c.transformPolicyToConf(ctx)
	c.installPath = android.PathForModuleInstall(ctx, "etc")
	ctx.InstallFile(c.installPath, c.stem(), c.installSource)

	ctx.SetOutputFiles(android.Paths{c.installSource}, "")

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

func (c *policyConf) AndroidMkEntries() []android.AndroidMkEntries {
	return []android.AndroidMkEntries{android.AndroidMkEntries{
		OutputFile: android.OptionalPathForPath(c.installSource),
		Class:      "ETC",
		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
				entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", !c.installable())
				entries.SetPath("LOCAL_MODULE_PATH", c.installPath)
				entries.SetString("LOCAL_INSTALLED_MODULE_STEM", c.stem())
			},
		},
	}}
}

type policyCilProperties struct {
	// Name of the output. Default is {module_name}
	Stem *string

	// Policy file to be compiled to cil file.
	Src *string `android:"path"`

	// If true, the input policy file is a binary policy that will be decompiled to a cil file.
	// Defaults to false.
	Decompile_binary *bool

	// Additional cil files to be added in the end of the output. This is to support workarounds
	// which are not supported by the policy language.
	Additional_cil_files []string `android:"path"`

	// Cil files to be filtered out by the filter_out tool of "build_sepolicy". Used to build
	// exported policies
	Filter_out []string `android:"path"`

	// Whether to run secilc to check compiled policy or not. Defaults to true
	Secilc_check *bool

	// Whether to ignore neverallow when running secilc check. Defaults to
	// SELINUX_IGNORE_NEVERALLOWS.
	Ignore_neverallow *bool

	// Whether this module is directly installable to one of the partitions. Default is true
	Installable *bool
}

type policyCil struct {
	android.ModuleBase

	properties policyCilProperties

	installSource android.Path
	installPath   android.InstallPath
}

// se_policy_cil compiles a policy.conf file to a cil file with checkpolicy, and optionally runs
// secilc to check the output cil file. Affected by SELINUX_IGNORE_NEVERALLOWS.
func policyCilFactory() android.Module {
	c := &policyCil{}
	c.AddProperties(&c.properties)
	android.InitAndroidArchModule(c, android.DeviceSupported, android.MultilibCommon)
	return c
}

func (c *policyCil) Installable() bool {
	return proptools.BoolDefault(c.properties.Installable, true)
}

func (c *policyCil) stem() string {
	return proptools.StringDefault(c.properties.Stem, c.Name())
}

func (c *policyCil) compileConfToCil(ctx android.ModuleContext, conf android.Path) android.OutputPath {
	cil := pathForModuleOut(ctx, c.stem())
	rule := android.NewRuleBuilder(pctx, ctx)
	checkpolicyCmd := rule.Command().BuiltTool("checkpolicy").
		Flag("-C"). // Write CIL
		Flag("-M"). // Enable MLS
		Flag("-L"). // Line markers for allow rules
		FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
		FlagWithOutput("-o ", cil).
		Input(conf)

	if proptools.Bool(c.properties.Decompile_binary) {
		checkpolicyCmd.Flag("-b") // Read binary
	}

	if len(c.properties.Filter_out) > 0 {
		rule.Command().BuiltTool("build_sepolicy").
			Text("filter_out").
			Flag("-f").
			Inputs(android.PathsForModuleSrc(ctx, c.properties.Filter_out)).
			FlagWithOutput("-t ", cil)
	}

	if len(c.properties.Additional_cil_files) > 0 {
		rule.Command().Text("cat").
			Inputs(android.PathsForModuleSrc(ctx, c.properties.Additional_cil_files)).
			Text(">> ").Output(cil)
	}

	if proptools.BoolDefault(c.properties.Secilc_check, true) {
		secilcCmd := rule.Command().BuiltTool("secilc").
			Flag("-m").                 // Multiple decls
			FlagWithArg("-M ", "true"). // Enable MLS
			Flag("-G").                 // expand and remove auto generated attributes
			FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
			Inputs(android.PathsForModuleSrc(ctx, c.properties.Filter_out)). // Also add cil files which are filtered out
			Text(cil.String()).
			FlagWithArg("-o ", os.DevNull).
			FlagWithArg("-f ", os.DevNull)

		if proptools.BoolDefault(c.properties.Ignore_neverallow, ctx.Config().SelinuxIgnoreNeverallows()) {
			secilcCmd.Flag("-N")
		}
	}

	rule.Build("cil", "Building cil for "+ctx.ModuleName())
	return cil
}

func (c *policyCil) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	if proptools.String(c.properties.Src) == "" {
		ctx.PropertyErrorf("src", "must be specified")
		return
	}
	conf := android.PathForModuleSrc(ctx, *c.properties.Src)
	cil := c.compileConfToCil(ctx, conf)

	if !c.Installable() {
		c.SkipInstall()
	}

	if c.InstallInDebugRamdisk() {
		// for userdebug_plat_sepolicy.cil
		c.installPath = android.PathForModuleInstall(ctx)
	} else {
		c.installPath = android.PathForModuleInstall(ctx, "etc", "selinux")
	}
	c.installSource = cil
	ctx.InstallFile(c.installPath, c.stem(), c.installSource)

	ctx.SetOutputFiles(android.Paths{c.installSource}, "")

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

func (c *policyCil) AndroidMkEntries() []android.AndroidMkEntries {
	return []android.AndroidMkEntries{android.AndroidMkEntries{
		OutputFile: android.OptionalPathForPath(c.installSource),
		Class:      "ETC",
		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
				entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", !c.Installable())
				entries.SetPath("LOCAL_MODULE_PATH", c.installPath)
				entries.SetString("LOCAL_INSTALLED_MODULE_STEM", c.stem())
			},
		},
	}}
}

type policyBinaryProperties struct {
	// Name of the output. Default is {module_name}
	Stem *string

	// Cil files to be compiled.
	Srcs []string `android:"path"`

	// Whether to ignore neverallow when running secilc check. Defaults to
	// SELINUX_IGNORE_NEVERALLOWS.
	Ignore_neverallow *bool

	// Whether this module is directly installable to one of the partitions. Default is true
	Installable *bool

	// List of domains that are allowed to be in permissive mode on user builds.
	Permissive_domains_on_user_builds []string
}

type policyBinary struct {
	android.ModuleBase

	properties policyBinaryProperties

	installSource android.Path
	installPath   android.InstallPath
}

// se_policy_binary compiles cil files to a binary sepolicy file with secilc.  Usually sources of
// se_policy_binary come from outputs of se_policy_cil modules.
func policyBinaryFactory() android.Module {
	c := &policyBinary{}
	c.AddProperties(&c.properties)
	android.InitAndroidArchModule(c, android.DeviceSupported, android.MultilibCommon)
	return c
}

func (c *policyBinary) InstallInRoot() bool {
	return c.InstallInRecovery()
}

func (c *policyBinary) Installable() bool {
	return proptools.BoolDefault(c.properties.Installable, true)
}

func (c *policyBinary) stem() string {
	return proptools.StringDefault(c.properties.Stem, c.Name())
}

func (c *policyBinary) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	if len(c.properties.Srcs) == 0 {
		ctx.PropertyErrorf("srcs", "must be specified")
		return
	}
	bin := pathForModuleOut(ctx, c.stem()+"_policy")
	rule := android.NewRuleBuilder(pctx, ctx)
	secilcCmd := rule.Command().BuiltTool("secilc").
		Flag("-m").                 // Multiple decls
		FlagWithArg("-M ", "true"). // Enable MLS
		Flag("-G").                 // expand and remove auto generated attributes
		FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
		Inputs(android.PathsForModuleSrc(ctx, c.properties.Srcs)).
		FlagWithOutput("-o ", bin).
		FlagWithArg("-f ", os.DevNull)

	if proptools.BoolDefault(c.properties.Ignore_neverallow, ctx.Config().SelinuxIgnoreNeverallows()) {
		secilcCmd.Flag("-N")
	}
	rule.Temporary(bin)

	// permissive check is performed only in user build (not debuggable).
	if !ctx.Config().Debuggable() {
		permissiveDomains := pathForModuleOut(ctx, c.stem()+"_permissive")
		cmd := rule.Command().BuiltTool("sepolicy-analyze").
			Input(bin).
			Text("permissive")
		// Filter-out domains listed in permissive_domains_on_user_builds
		allowedDomains := c.properties.Permissive_domains_on_user_builds
		if len(allowedDomains) != 0 {
			cmd.Text("| { grep -Fxv")
			for _, d := range allowedDomains {
				cmd.FlagWithArg("-e ", proptools.ShellEscape(d))
			}
			cmd.Text(" || true; }") // no match doesn't fail the cmd
		}
		cmd.Text(" > ").Output(permissiveDomains)
		rule.Temporary(permissiveDomains)

		msg := `==========\n` +
			`ERROR: permissive domains not allowed in user builds\n` +
			`List of invalid domains:`

		rule.Command().Text("if test").
			FlagWithInput("-s ", permissiveDomains).
			Text("; then echo").
			Flag("-e").
			Text(`"` + msg + `"`).
			Text("&& cat ").
			Input(permissiveDomains).
			Text("; exit 1; fi")
	}

	out := pathForModuleOut(ctx, c.stem())
	rule.Command().Text("cp").
		Flag("-f").
		Input(bin).
		Output(out)

	rule.DeleteTemporaryFiles()
	rule.Build("secilc", "Compiling cil files for "+ctx.ModuleName())

	if !c.Installable() {
		c.SkipInstall()
	}

	if c.InstallInRecovery() {
		// install in root
		c.installPath = android.PathForModuleInstall(ctx)
	} else {
		c.installPath = android.PathForModuleInstall(ctx, "etc", "selinux")
	}
	c.installSource = out
	ctx.InstallFile(c.installPath, c.stem(), c.installSource)

	ctx.SetOutputFiles(android.Paths{c.installSource}, "")

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

func (c *policyBinary) AndroidMkEntries() []android.AndroidMkEntries {
	return []android.AndroidMkEntries{android.AndroidMkEntries{
		OutputFile: android.OptionalPathForPath(c.installSource),
		Class:      "ETC",
		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
				entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", !c.Installable())
				entries.SetPath("LOCAL_MODULE_PATH", c.installPath)
				entries.SetString("LOCAL_INSTALLED_MODULE_STEM", c.stem())
			},
		},
	}}
}
