// Copyright (c) Contributors to the Apptainer project, established as
//   Apptainer a Series of LF Projects LLC.
//   For website terms of use, trademark policy, privacy policy and other
//   project policies see https://lfprojects.org/policies
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"strings"
	"text/template"

	"github.com/apptainer/apptainer/pkg/sylog"
)

func parseLine(s string) (d Define) {
	d = Define{
		Words: strings.Fields(s),
	}

	return
}

// Define is a struct that contains one line of configuration words.
type Define struct {
	Words []string
}

// WriteLine writes a line of configuration.
func (d Define) WriteLine() (s string) {
	s = d.Words[2]
	if len(d.Words) > 3 {
		for _, w := range d.Words[3:] {
			s += " + " + w
		}
	}

	varType := "const"
	varStatement := d.Words[1] + " = " + s

	// Apply runtime relocation to some variables
	switch d.Words[1] {
	case
		"BINDIR",
		"LIBEXECDIR",
		"SYSCONFDIR",
		"SESSIONDIR",
		"APPTAINER_CONFDIR",
		"PLUGIN_ROOTDIR":
		varType = "var"
		varStatement = d.Words[1] + " = relocatePath(" + s + ")"
	case "APPTAINER_SUID_INSTALL":
		varType = "var"
		varStatement = d.Words[1] + " = isSuidInstall()"
	default:
		if strings.Contains(s, "APPTAINER_CONFDIR") {
			varType = "var"
		}
	}

	return varType + " " + varStatement
}

var confgenTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
package buildcfg

import (
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/apptainer/apptainer/pkg/sylog"
)

var (
	prefixOnce    sync.Once
	installPrefix string
	isSuidOnce    sync.Once
	suidInstall   int
)

func getPrefix() (string) {
	// NOTE: the first time this is called (from isSuidInstall()) is very
	// early, and some error conditions may happen before debug messages
	// are enabled.  Warnings and info messages do still work at that point.
	prefixOnce.Do(func() {
		// Although this is a sync.Once, there are multiple address
		// spaces using this code so it does get called more than once
		installPrefix = "{{.Prefix}}"
		executablePath, err := os.Executable()
		if err != nil {
			sylog.Warningf("Error getting executable path, using default: %v", err)
			return
		}

		sylog.Debugf("executablePath is %v", executablePath)
		_, err = os.Stat(executablePath)
		if err != nil {
			// Due to mount namespace issues, os.Executable may return a non-existing
			// location.  This is normal when starter-suid is in its compiled location,
			// but assuming the original prefix here may help also in other circumstances.
			// See https://github.com/apptainer/apptainer/issues/1061
			sylog.Debugf("executablePath does not exist, assuming default prefix")
			return
		}

		bin := filepath.Dir(executablePath)
		base := filepath.Base(executablePath)

		switch base {
		case "apptainer":
			realBindir, err := filepath.EvalSymlinks("{{.Bindir}}")
			if err != nil {
				sylog.Debugf("Error finding real path of Bindir, assuming %v was relocated: %v", base, err)
				installPrefix = filepath.Dir(bin)
			} else if bin == realBindir {
				// apptainer binary was not relocated
				sylog.Debugf("%v was not relocated from %v", base, realBindir)
			} else {
				// PREFIX/bin/apptainer
				sylog.Debugf("%v was relocated because %v != %v", base, bin, realBindir)
				installPrefix = filepath.Dir(bin)
			}
		case "starter":
			// The default LIBEXECDIR is PREFIX/libexec
			// LIBEXECDIR/apptainer/bin/starter
			installLibexecdir := filepath.Dir(filepath.Dir(bin))
			realLibexecdir, err := filepath.EvalSymlinks("{{.Libexecdir}}")
			if err != nil {
				sylog.Debugf("Error finding real path of Libexecdir, assuming %v was relocated: %v", base, err)
				installPrefix = filepath.Dir(installLibexecdir)
			} else if installLibexecdir == realLibexecdir {
				// starter was not relocated
				sylog.Debugf("%v was not relocated from %v", base, realLibexecdir)
			} else {
				sylog.Debugf("%v was relocated because %v != %v", base, installLibexecdir, realLibexecdir)
				installPrefix = filepath.Dir(installLibexecdir)
			}
		case "starter-suid":
			sylog.Debugf("Base is starter-suid which never relocates")
		default:
			sylog.Debugf("Unrecognized base program name %v, skipping relocate", base)
		}
		sylog.Debugf("Install prefix is %s", installPrefix)
	})
	return installPrefix
}

// This needs to be a Once to avoid a possible race condition attack.
// Otherwise it is possible to let it fail to find the starter-suid the first
// attempt and then slip in a symlink to a setuid starter-suid elsewhere,
// and fool it into using an attacker-controlled configuration file.
func isSuidInstall() int {
	isSuidOnce.Do(func() {
		path := getPrefix()
		if path == "{{.Prefix}}" {
			path = "{{.Libexecdir}}"
		} else {
			path += "/libexec"
		}
		path += "/apptainer/bin/starter-suid"
		_, err := os.Stat(path)
		if err == nil {
			suidInstall = 1
		}
	})
	return suidInstall
}

func relocatePath(original string) string {
	if "{{.Prefix}}" == "" || "{{.Prefix}}" == "/" {
		return original
	}
	rootPrefix := false
	if !strings.HasPrefix(original, "{{.Prefix}}") {
		if strings.HasPrefix(original, "/etc/apptainer") ||
			strings.HasPrefix(original, "/var/apptainer") ||
			strings.HasPrefix(original, "/var/lib/apptainer") {
			// These are typically the only pieces not under
			// "/usr" (which is the prefix) in packages
			rootPrefix = true
		} else {
			return original
		}
	}

	prefix := getPrefix()
	if prefix == "{{.Prefix}}" {
		return original
	}

	if isSuidInstall() == 1 {
		// For security reasons, do not relocate when there
		// is a starter-suid
		sylog.Fatalf("Relocation not allowed with starter-suid")
	}

	var relativePath string
	var err error
	if rootPrefix {
		relativePath, err = filepath.Rel("/", original)
	} else {
		relativePath, err = filepath.Rel("{{.Prefix}}", original)
	}
	if err != nil {
		sylog.Fatalf("%s", err.Error())
	}

	result := filepath.Join(prefix, relativePath)
	return result
}

{{ range $i, $d := .Defines }}
{{$d.WriteLine -}}
{{end}}

func IsReproducibleBuild() bool {
	return SOURCEDIR == "REPRODUCIBLE_BUILD"
}
`))

func main() {
	outFile, err := os.Create("config.go")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer outFile.Close()

	// Parse the config.h file
	inFile, err := os.ReadFile(os.Args[1])
	if err != nil {
		fmt.Println(err)
		return
	}

	header := []Define{}
	s := bufio.NewScanner(bytes.NewReader(inFile))
	vars := []string{"PREFIX", "BINDIR", "LIBEXECDIR"}
	vals := []string{"", "", ""}
	for s.Scan() {
		d := parseLine(s.Text())
		if len(d.Words) > 2 && d.Words[0] == "#define" {
			for idx, configVar := range vars {
				if d.Words[1] == configVar {
					if len(d.Words) != 3 {
						sylog.Fatalf("Expected %s to contain 3 elements", configVar)
					}
					vals[idx] = d.Words[2] //nolint:gosec
				}
			}
			header = append(header, d)
		}
	}
	for idx, configVar := range vars {
		if vals[idx] == "" {
			sylog.Fatalf("Failed to find value of %s", configVar)
		}
	}
	prefix := vals[0]
	bindir := vals[1]
	libexecdir := vals[2]

	if goBuildTags := os.Getenv("GO_BUILD_TAGS"); goBuildTags != "" {
		d := Define{
			Words: []string{
				"#define",
				"GO_BUILD_TAGS",
				fmt.Sprintf("`%s`", goBuildTags),
			},
		}
		header = append(header, d)
	}

	data := struct {
		Prefix     string
		Bindir     string
		Libexecdir string
		Defines    []Define
	}{
		prefix[1 : len(prefix)-1],
		bindir[1 : len(bindir)-1],
		libexecdir[1 : len(libexecdir)-1],
		header,
	}
	err = confgenTemplate.Execute(outFile, data)
	if err != nil {
		fmt.Println(err)
		return
	}
}
