// 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"
	"strconv"

	"github.com/google/blueprint/proptools"

	"android/soong/android"
)

func init() {
	android.RegisterModuleType("se_versioned_policy", versionedPolicyFactory)
}

type versionedPolicyProperties struct {
	// Base cil file for versioning.
	Base *string `android:"path"`

	// Output file name. Defaults to {name} if target_policy is set, {version}.cil if mapping is set
	Stem *string

	// Target sepolicy version. Can be a specific version number (e.g. "30.0" for R), "current"
	// (PLATFORM_SEPOLICY_VERSION), or "vendor" (BOARD_SEPOLICY_VERS). Defaults to "current"
	Version *string

	// If true, generate mapping file from given base cil file. Cannot be set with target_policy.
	Mapping *bool

	// If given, version target policy file according to base policy. Cannot be set with mapping.
	Target_policy *string `android:"path"`

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

	// Cil files to which this mapping file depends. If specified, secilc checks whether the output
	// file can be merged with specified cil files or not.
	Dependent_cils []string `android:"path"`

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

	// install to a subdirectory of the default install path for the module
	Relative_install_path *string
}

type versionedPolicy struct {
	android.ModuleBase

	properties versionedPolicyProperties

	installSource android.Path
	installPath   android.InstallPath
}

// se_versioned_policy generates versioned cil file with "version_policy". This can generate either
// mapping file for public plat policies, or associate a target policy file with the version that
// non-platform policy targets.
func versionedPolicyFactory() android.Module {
	m := &versionedPolicy{}
	m.AddProperties(&m.properties)
	android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
	return m
}

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

func (m *versionedPolicy) DepsMutator(ctx android.BottomUpMutatorContext) {
	// do nothing
}

func (m *versionedPolicy) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	version := proptools.StringDefault(m.properties.Version, "current")
	if version == "current" {
		version = ctx.DeviceConfig().PlatformSepolicyVersion()
	} else if version == "vendor" {
		version = ctx.DeviceConfig().BoardSepolicyVers()
	}

	var stem string
	if s := proptools.String(m.properties.Stem); s != "" {
		stem = s
	} else if proptools.Bool(m.properties.Mapping) {
		stem = version + ".cil"
	} else {
		stem = ctx.ModuleName()
	}

	out := pathForModuleOut(ctx, stem)
	rule := android.NewRuleBuilder(pctx, ctx)

	if proptools.String(m.properties.Base) == "" {
		ctx.PropertyErrorf("base", "must be specified")
		return
	}

	versionCmd := rule.Command().BuiltTool("version_policy").
		FlagWithInput("-b ", android.PathForModuleSrc(ctx, *m.properties.Base)).
		FlagWithArg("-n ", version).
		FlagWithOutput("-o ", out)

	if proptools.Bool(m.properties.Mapping) && proptools.String(m.properties.Target_policy) != "" {
		ctx.ModuleErrorf("Can't set both mapping and target_policy")
		return
	}

	if proptools.Bool(m.properties.Mapping) {
		versionCmd.Flag("-m")
	} else if target := proptools.String(m.properties.Target_policy); target != "" {
		versionCmd.FlagWithInput("-t ", android.PathForModuleSrc(ctx, target))
	} else {
		ctx.ModuleErrorf("Either mapping or target_policy must be set")
		return
	}

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

	if len(m.properties.Dependent_cils) > 0 {
		rule.Command().BuiltTool("secilc").
			Flag("-m").
			FlagWithArg("-M ", "true").
			Flag("-G").
			Flag("-N").
			FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
			Inputs(android.PathsForModuleSrc(ctx, m.properties.Dependent_cils)).
			Text(out.String()).
			FlagWithArg("-o ", os.DevNull).
			FlagWithArg("-f ", os.DevNull)
	}

	rule.Build("mapping", "Versioning mapping file "+ctx.ModuleName())

	if !m.installable() {
		m.SkipInstall()
	}

	m.installSource = out
	m.installPath = android.PathForModuleInstall(ctx, "etc", "selinux")
	if subdir := proptools.String(m.properties.Relative_install_path); subdir != "" {
		m.installPath = m.installPath.Join(ctx, subdir)
	}
	ctx.InstallFile(m.installPath, m.installSource.Base(), m.installSource)

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

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