// Copyright (C) 2018 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

// This file contains "se_cil_compat_map" module type used to build and install
// sepolicy backwards compatibility mapping files.

import (
	"android/soong/android"
	"fmt"
	"io"

	"github.com/google/blueprint"
	"github.com/google/blueprint/proptools"
)

var (
	pctx = android.NewPackageContext("android/soong/selinux")

	combine_maps    = pctx.HostBinToolVariable("combine_maps", "combine_maps")
	combineMapsCmd  = "${combine_maps} -t ${topHalf} -b ${bottomHalf} -o $out"
	combineMapsRule = pctx.StaticRule(
		"combineMapsRule",
		blueprint.RuleParams{
			Command:     combineMapsCmd,
			CommandDeps: []string{"${combine_maps}"},
		},
		"topHalf",
		"bottomHalf",
	)

	String        = proptools.String
	TopHalfDepTag = dependencyTag{name: "top"}
)

func init() {
	android.RegisterModuleType("se_cil_compat_map", cilCompatMapFactory)
	pctx.Import("android/soong/android")
}

func cilCompatMapFactory() android.Module {
	c := &cilCompatMap{}
	c.AddProperties(&c.properties)
	android.InitAndroidModule(c)
	return c
}

type cilCompatMapProperties struct {
	// se_cil_compat_map module representing a compatibility mapping file for
	// platform versions (x->y). Bottom half represents a mapping (y->z).
	// Together the halves are used to generate a (x->z) mapping.
	Top_half *string
	// list of source (.cil) files used to build an the bottom half of sepolicy
	// compatibility mapping file. bottom_half may reference the outputs of
	// other modules that produce source files like genrule or filegroup using
	// the syntax ":module". srcs has to be non-empty.
	Bottom_half []string
}

type cilCompatMap struct {
	android.ModuleBase
	properties cilCompatMapProperties
	// (.intermediate) module output path as installation source.
	installSource android.Path
}

type CilCompatMapGenerator interface {
	GeneratedMapFile() android.Path
}

type dependencyTag struct {
	blueprint.BaseDependencyTag
	name string
}

func expandTopHalf(ctx android.ModuleContext) android.OptionalPath {
	var topHalf android.OptionalPath
	ctx.VisitDirectDeps(func(dep android.Module) {
		depTag := ctx.OtherModuleDependencyTag(dep)
		switch depTag {
		case TopHalfDepTag:
			topHalf = android.OptionalPathForPath(dep.(CilCompatMapGenerator).GeneratedMapFile())
		}
	})
	return topHalf
}

func expandSeSources(ctx android.ModuleContext, srcFiles []string) android.Paths {
	expandedSrcFiles := make(android.Paths, 0, len(srcFiles))
	for _, s := range srcFiles {
		if m := android.SrcIsModule(s); m != "" {
			module := ctx.GetDirectDepWithTag(m, android.SourceDepTag)
			if module == nil {
				// Error will have been handled by ExtractSourcesDeps
				continue
			}
			if fg, ok := module.(*fileGroup); ok {
				// Core compatibility mapping files are under system/sepolicy/private.
				expandedSrcFiles = append(expandedSrcFiles, fg.SystemPrivateSrcs()...)
				// Partner extensions to the compatibility mapping in must be located in
				// BOARD_PLAT_PRIVATE_SEPOLICY_DIR
				expandedSrcFiles = append(expandedSrcFiles, fg.SystemExtPrivateSrcs()...)
			} else {
				ctx.ModuleErrorf("srcs dependency %q is not an selinux filegroup", m)
			}
		} else {
			p := android.PathForModuleSrc(ctx, s)
			expandedSrcFiles = append(expandedSrcFiles, p)
		}
	}
	return expandedSrcFiles
}

func (c *cilCompatMap) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	srcFiles := expandSeSources(ctx, c.properties.Bottom_half)

	for _, src := range srcFiles {
		if src.Ext() != ".cil" {
			ctx.PropertyErrorf("bottom_half", "%s has to be a .cil file.", src.String())
		}
	}

	bottomHalf := android.PathForModuleGen(ctx, "bottom_half")
	ctx.Build(pctx, android.BuildParams{
		Rule:   android.Cat,
		Output: bottomHalf,
		Inputs: srcFiles,
	})

	topHalf := expandTopHalf(ctx)
	if topHalf.Valid() {
		out := android.PathForModuleGen(ctx, c.Name())
		ctx.ModuleBuild(pctx, android.ModuleBuildParams{
			Rule:   combineMapsRule,
			Output: out,
			Implicits: []android.Path{
				topHalf.Path(),
				bottomHalf,
			},
			Args: map[string]string{
				"topHalf":    topHalf.String(),
				"bottomHalf": bottomHalf.String(),
			},
		})
		c.installSource = out
	} else {
		c.installSource = bottomHalf
	}
}

func (c *cilCompatMap) DepsMutator(ctx android.BottomUpMutatorContext) {
	android.ExtractSourcesDeps(ctx, c.properties.Bottom_half)
	if c.properties.Top_half != nil {
		ctx.AddDependency(c, TopHalfDepTag, String(c.properties.Top_half))
	}
}

func (c *cilCompatMap) AndroidMk() android.AndroidMkData {
	ret := android.AndroidMkData{
		OutputFile: android.OptionalPathForPath(c.installSource),
		Class:      "ETC",
	}
	ret.Extra = append(ret.Extra, func(w io.Writer, outputFile android.Path) {
		fmt.Fprintln(w, "LOCAL_MODULE_PATH := $(TARGET_OUT)/etc/selinux/mapping")
	})
	return ret
}

var _ CilCompatMapGenerator = (*cilCompatMap)(nil)

func (c *cilCompatMap) GeneratedMapFile() android.Path {
	return c.installSource
}