package selinux

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"math/big"
	"math/rand/v2"
	"os"
	"os/user"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"sync"

	"github.com/cyphar/filepath-securejoin/pathrs-lite"
	"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
	"golang.org/x/sys/unix"

	"github.com/opencontainers/selinux/pkg/pwalkdir"
)

const (
	minSensLen       = 2
	contextFile      = "/usr/share/containers/selinux/contexts"
	selinuxDir       = "/etc/selinux/"
	selinuxUsersDir  = "contexts/users"
	defaultContexts  = "contexts/default_contexts"
	failsafeContext  = "contexts/failsafe_context"
	selinuxConfig    = selinuxDir + "config"
	selinuxfsMount   = "/sys/fs/selinux"
	selinuxTypeTag   = "SELINUXTYPE"
	selinuxTag       = "SELINUX"
	xattrNameSelinux = "security.selinux"
)

type selinuxState struct {
	mcsList    map[string]struct{}
	enabledSet bool
	enabled    bool
	sync.Mutex
}

type level struct {
	cats *big.Int
	sens int
}

type mlsRange struct {
	low  *level
	high *level
}

type openReaderCloser func() (io.ReadCloser, error)

func createOpener(path string) openReaderCloser {
	return func() (io.ReadCloser, error) {
		return os.Open(path)
	}
}

type defaultSECtx struct {
	openUserRdr       openReaderCloser
	verifier          func(string) error
	openDefaultRdr    openReaderCloser
	openFailsafeRdr   openReaderCloser
	user, level, scon string
}

type levelItem byte

const (
	sensitivity levelItem = 's'
	category    levelItem = 'c'
)

var (
	readOnlyFileLabel string

	state = selinuxState{
		mcsList: make(map[string]struct{}),
	}
)

var policyRoot = sync.OnceValue(func() string {
	return filepath.Join(selinuxDir, readConfig(selinuxTypeTag))
})

func (s *selinuxState) setEnable(enabled bool) bool {
	s.Lock()
	defer s.Unlock()
	s.enabledSet = true
	s.enabled = enabled
	return s.enabled
}

func (s *selinuxState) getEnabled() bool {
	s.Lock()
	enabled := s.enabled
	enabledSet := s.enabledSet
	s.Unlock()
	if enabledSet {
		return enabled
	}

	enabled = false
	if fs := getSelinuxMountPoint(); fs != "" {
		if con, _ := CurrentLabel(); con != "kernel" {
			enabled = true
		}
	}
	return s.setEnable(enabled)
}

// setDisabled disables SELinux support for the package
func setDisabled() {
	state.setEnable(false)
}

func verifySELinuxfsMount(mnt string) bool {
	var buf unix.Statfs_t
	for {
		err := unix.Statfs(mnt, &buf)
		if err == nil {
			break
		}
		if err == unix.EAGAIN || err == unix.EINTR {
			continue
		}
		return false
	}

	//#nosec G115 -- there is no overflow here.
	if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) {
		return false
	}
	if (buf.Flags & unix.ST_RDONLY) != 0 {
		return false
	}

	return true
}

// getSelinuxMountPoint returns the path to the mountpoint of an selinuxfs
// filesystem or an empty string if no mountpoint is found.  Selinuxfs is
// a proc-like pseudo-filesystem that exposes the SELinux policy API to
// processes.  The existence of an selinuxfs mount is used to determine
// whether SELinux is currently enabled or not.
var getSelinuxMountPoint = sync.OnceValue(func() string {
	// fast path: check the default mount first
	if verifySELinuxfsMount(selinuxfsMount) {
		return selinuxfsMount
	}

	// check if selinuxfs is available before going the slow path
	fs, err := os.ReadFile("/proc/filesystems")
	if err != nil {
		return ""
	}
	if !bytes.Contains(fs, []byte("\tselinuxfs\n")) {
		return ""
	}

	// slow path: try to find among the mounts
	f, err := os.Open("/proc/self/mountinfo")
	if err != nil {
		return ""
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for {
		mnt := findSELinuxfsMount(scanner)
		if mnt == "" { // error or not found
			return ""
		}
		if verifySELinuxfsMount(mnt) {
			return mnt
		}
	}
})

// findSELinuxfsMount returns a next selinuxfs mount point found,
// if there is one, or an empty string in case of EOF or error.
func findSELinuxfsMount(s *bufio.Scanner) string {
	for s.Scan() {
		txt := s.Bytes()
		// The first field after - is fs type.
		// Safe as spaces in mountpoints are encoded as \040
		if !bytes.Contains(txt, []byte(" - selinuxfs ")) {
			continue
		}
		const mPos = 5 // mount point is 5th field
		fields := bytes.SplitN(txt, []byte(" "), mPos+1)
		if len(fields) < mPos+1 {
			continue
		}
		return string(fields[mPos-1])
	}

	return ""
}

// getEnabled returns whether SELinux is currently enabled.
func getEnabled() bool {
	return state.getEnabled()
}

func readConfig(target string) string {
	in, err := os.Open(selinuxConfig)
	if err != nil {
		return ""
	}
	defer in.Close()

	scanner := bufio.NewScanner(in)

	for scanner.Scan() {
		line := bytes.TrimSpace(scanner.Bytes())
		if len(line) == 0 {
			// Skip blank lines
			continue
		}
		if line[0] == ';' || line[0] == '#' {
			// Skip comments
			continue
		}
		key, val, ok := bytes.Cut(line, []byte{'='})
		if ok && string(key) == target {
			return string(bytes.Trim(val, `"`))
		}
	}
	return ""
}

func readConFd(in *os.File) (string, error) {
	data, err := io.ReadAll(in)
	if err != nil {
		return "", err
	}
	return string(bytes.TrimSuffix(data, []byte{0})), nil
}

func writeConFd(out *os.File, val string) error {
	var err error
	if val != "" {
		_, err = out.Write([]byte(val))
	} else {
		_, err = out.Write(nil)
	}
	return err
}

// openProcThreadSelf is a small wrapper around [procfs.Handle.OpenThreadSelf]
// and [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
// provided mode must be os.O_* flags to indicate what mode the returned file
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
// supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/thread-self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) {
	if subpath == "" {
		return nil, nil, ErrEmptyPath
	}

	proc, err := procfs.OpenProcRoot()
	if err != nil {
		return nil, nil, err
	}
	defer proc.Close()

	handle, closer, err := proc.OpenThreadSelf(subpath)
	if err != nil {
		return nil, nil, fmt.Errorf("open /proc/thread-self/%s handle: %w", subpath, err)
	}
	defer handle.Close() // we will return a re-opened handle

	file, err := pathrs.Reopen(handle, mode)
	if err != nil {
		closer()
		return nil, nil, fmt.Errorf("reopen /proc/thread-self/%s handle (%#x): %w", subpath, mode, err)
	}
	return file, closer, nil
}

// Read the contents of /proc/thread-self/<fpath>.
func readConThreadSelf(fpath string) (string, error) {
	in, closer, err := openProcThreadSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
	if err != nil {
		return "", err
	}
	defer closer()
	defer in.Close()

	return readConFd(in)
}

// Write <val> to /proc/thread-self/<fpath>.
func writeConThreadSelf(fpath, val string) error {
	if val == "" {
		if !getEnabled() {
			return nil
		}
	}

	out, closer, err := openProcThreadSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
	if err != nil {
		return err
	}
	defer closer()
	defer out.Close()

	return writeConFd(out, val)
}

// openProcSelf is a small wrapper around [procfs.Handle.OpenSelf] and
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
// provided mode must be os.O_* flags to indicate what mode the returned file
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
// supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
func openProcSelf(subpath string, mode int) (*os.File, error) {
	if subpath == "" {
		return nil, ErrEmptyPath
	}

	proc, err := procfs.OpenProcRoot()
	if err != nil {
		return nil, err
	}
	defer proc.Close()

	handle, err := proc.OpenSelf(subpath)
	if err != nil {
		return nil, fmt.Errorf("open /proc/self/%s handle: %w", subpath, err)
	}
	defer handle.Close() // we will return a re-opened handle

	file, err := pathrs.Reopen(handle, mode)
	if err != nil {
		return nil, fmt.Errorf("reopen /proc/self/%s handle (%#x): %w", subpath, mode, err)
	}
	return file, nil
}

// Read the contents of /proc/self/<fpath>.
func readConSelf(fpath string) (string, error) {
	in, err := openProcSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
	if err != nil {
		return "", err
	}
	defer in.Close()

	return readConFd(in)
}

// Write <val> to /proc/self/<fpath>.
func writeConSelf(fpath, val string) error {
	if val == "" {
		if !getEnabled() {
			return nil
		}
	}

	out, err := openProcSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
	if err != nil {
		return err
	}
	defer out.Close()

	return writeConFd(out, val)
}

// openProcPid is a small wrapper around [procfs.Handle.OpenPid] and
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
// provided mode must be os.O_* flags to indicate what mode the returned file
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
// supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
func openProcPid(pid int, subpath string, mode int) (*os.File, error) {
	if subpath == "" {
		return nil, ErrEmptyPath
	}

	proc, err := procfs.OpenProcRoot()
	if err != nil {
		return nil, err
	}
	defer proc.Close()

	handle, err := proc.OpenPid(pid, subpath)
	if err != nil {
		return nil, fmt.Errorf("open /proc/%d/%s handle: %w", pid, subpath, err)
	}
	defer handle.Close() // we will return a re-opened handle

	file, err := pathrs.Reopen(handle, mode)
	if err != nil {
		return nil, fmt.Errorf("reopen /proc/%d/%s handle (%#x): %w", pid, subpath, mode, err)
	}
	return file, nil
}

// classIndex returns the int index for an object class in the loaded policy,
// or -1 and an error
func classIndex(class string) (int, error) {
	permpath := fmt.Sprintf("class/%s/index", class)
	indexpath := filepath.Join(getSelinuxMountPoint(), permpath)

	indexB, err := os.ReadFile(indexpath)
	if err != nil {
		return -1, err
	}
	index, err := strconv.Atoi(string(indexB))
	if err != nil {
		return -1, err
	}

	return index, nil
}

// lSetFileLabel sets the SELinux label for this path, not following symlinks,
// or returns an error.
func lSetFileLabel(fpath string, label string) error {
	if fpath == "" {
		return ErrEmptyPath
	}
	for {
		err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0)
		if err == nil {
			break
		}
		if err != unix.EINTR {
			return &os.PathError{Op: fmt.Sprintf("lsetxattr(label=%s)", label), Path: fpath, Err: err}
		}
	}

	return nil
}

// setFileLabel sets the SELinux label for this path, following symlinks,
// or returns an error.
func setFileLabel(fpath string, label string) error {
	if fpath == "" {
		return ErrEmptyPath
	}
	for {
		err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0)
		if err == nil {
			break
		}
		if err != unix.EINTR {
			return &os.PathError{Op: fmt.Sprintf("setxattr(label=%s)", label), Path: fpath, Err: err}
		}
	}

	return nil
}

// fileLabel returns the SELinux label for this path, following symlinks,
// or returns an error.
func fileLabel(fpath string) (string, error) {
	if fpath == "" {
		return "", ErrEmptyPath
	}

	label, err := getxattr(fpath, xattrNameSelinux)
	if err != nil {
		return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err}
	}
	// Trim the NUL byte at the end of the byte buffer, if present.
	if len(label) > 0 && label[len(label)-1] == '\x00' {
		label = label[:len(label)-1]
	}
	return string(label), nil
}

// lFileLabel returns the SELinux label for this path, not following symlinks,
// or returns an error.
func lFileLabel(fpath string) (string, error) {
	if fpath == "" {
		return "", ErrEmptyPath
	}

	label, err := lgetxattr(fpath, xattrNameSelinux)
	if err != nil {
		return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err}
	}
	// Trim the NUL byte at the end of the byte buffer, if present.
	if len(label) > 0 && label[len(label)-1] == '\x00' {
		label = label[:len(label)-1]
	}
	return string(label), nil
}

func setFSCreateLabel(label string) error {
	return writeConThreadSelf("attr/fscreate", label)
}

// pidLabel returns the SELinux label of the given pid, or an error.
func pidLabel(pid int) (string, error) {
	it, err := openProcPid(pid, "attr/current", os.O_RDONLY|unix.O_CLOEXEC)
	if err != nil {
		return "", nil
	}
	defer it.Close()
	return readConFd(it)
}

// canonicalizeContext takes a context string and writes it to the kernel
// the function then returns the context that the kernel will use. Use this
// function to check if two contexts are equivalent
func canonicalizeContext(val string) (string, error) {
	return readWriteCon(filepath.Join(getSelinuxMountPoint(), "context"), val)
}

// computeCreateContext requests the type transition from source to target for
// class from the kernel.
func computeCreateContext(source string, target string, class string) (string, error) {
	classidx, err := classIndex(class)
	if err != nil {
		return "", err
	}

	return readWriteCon(filepath.Join(getSelinuxMountPoint(), "create"), fmt.Sprintf("%s %s %d", source, target, classidx))
}

// catsToBitset stores categories in a bitset.
func catsToBitset(cats string) (*big.Int, error) {
	bitset := new(big.Int)

	catlist := strings.Split(cats, ",")
	for _, r := range catlist {
		if s, e, ok := strings.Cut(r, "."); ok {
			catstart, err := parseLevelItem(s, category)
			if err != nil {
				return nil, err
			}
			catend, err := parseLevelItem(e, category)
			if err != nil {
				return nil, err
			}
			for i := catstart; i <= catend; i++ {
				bitset.SetBit(bitset, i, 1)
			}
		} else {
			cat, err := parseLevelItem(r, category)
			if err != nil {
				return nil, err
			}
			bitset.SetBit(bitset, cat, 1)
		}
	}

	return bitset, nil
}

// parseLevelItem parses and verifies that a sensitivity or category are valid
func parseLevelItem(s string, sep levelItem) (int, error) {
	if len(s) < minSensLen || levelItem(s[0]) != sep {
		return 0, ErrLevelSyntax
	}
	const bitSize = 31 // Make sure the result fits into signed int32.
	val, err := strconv.ParseUint(s[1:], 10, bitSize)
	if err != nil {
		return 0, err
	}

	return int(val), nil
}

// parseLevel fills a level from a string that contains
// a sensitivity and categories
func (l *level) parseLevel(levelStr string) error {
	s, c, ok := strings.Cut(levelStr, ":")
	sens, err := parseLevelItem(s, sensitivity)
	if err != nil {
		return fmt.Errorf("failed to parse sensitivity: %w", err)
	}
	l.sens = sens
	if ok {
		cats, err := catsToBitset(c)
		if err != nil {
			return fmt.Errorf("failed to parse categories: %w", err)
		}
		l.cats = cats
	}

	return nil
}

// rangeStrToMLSRange marshals a string representation of a range.
func rangeStrToMLSRange(rangeStr string) (*mlsRange, error) {
	r := &mlsRange{}
	lo, hi, ok := strings.Cut(rangeStr, "-")
	r.low = &level{}
	if err := r.low.parseLevel(lo); err != nil {
		return nil, fmt.Errorf("failed to parse low level %q: %w", lo, err)
	}
	if ok {
		// rangeStr that has a low and a high level, e.g. s4:c0.c1023-s6:c0.c1023.
		r.high = &level{}
		if err := r.high.parseLevel(hi); err != nil {
			return nil, fmt.Errorf("failed to parse high level %q: %w", hi, err)
		}
	} else {
		// rangeStr that is single level, e.g. s6:c0,c3,c5,c30.c1023.
		r.high = r.low
	}

	return r, nil
}

// bitsetToStr takes a category bitset and returns it in the
// canonical selinux syntax
func bitsetToStr(c *big.Int) string {
	var str string

	length := 0
	i0 := int(c.TrailingZeroBits()) //#nosec G115 -- don't expect TralingZeroBits to return values with highest bit set.
	for i := i0; i < c.BitLen(); i++ {
		if c.Bit(i) == 0 {
			continue
		}
		if length == 0 {
			if str != "" {
				str += ","
			}
			str += "c" + strconv.Itoa(i)
		}
		if c.Bit(i+1) == 1 {
			length++
			continue
		}
		if length == 1 {
			str += ",c" + strconv.Itoa(i)
		} else if length > 1 {
			str += ".c" + strconv.Itoa(i)
		}
		length = 0
	}

	return str
}

func (l *level) equal(l2 *level) bool {
	if l2 == nil || l == nil {
		return l == l2
	}
	if l2.sens != l.sens {
		return false
	}
	if l2.cats == nil || l.cats == nil {
		return l2.cats == l.cats
	}
	return l.cats.Cmp(l2.cats) == 0
}

// String returns an mlsRange as a string.
func (m mlsRange) String() string {
	low := "s" + strconv.Itoa(m.low.sens)
	if m.low.cats != nil && m.low.cats.BitLen() > 0 {
		low += ":" + bitsetToStr(m.low.cats)
	}

	if m.low.equal(m.high) {
		return low
	}

	high := "s" + strconv.Itoa(m.high.sens)
	if m.high.cats != nil && m.high.cats.BitLen() > 0 {
		high += ":" + bitsetToStr(m.high.cats)
	}

	return low + "-" + high
}

// calculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound)
// of a source and target range.
// The glblub is calculated as the greater of the low sensitivities and
// the lower of the high sensitivities and the and of each category bitset.
func calculateGlbLub(sourceRange, targetRange string) (string, error) {
	s, err := rangeStrToMLSRange(sourceRange)
	if err != nil {
		return "", err
	}
	t, err := rangeStrToMLSRange(targetRange)
	if err != nil {
		return "", err
	}

	if s.high.sens < t.low.sens || t.high.sens < s.low.sens {
		/* these ranges have no common sensitivities */
		return "", ErrIncomparable
	}

	outrange := &mlsRange{low: &level{}, high: &level{}}

	/* take the greatest of the low */
	outrange.low.sens = max(s.low.sens, t.low.sens)

	/* take the least of the high */
	outrange.high.sens = min(s.high.sens, t.high.sens)

	/* find the intersecting categories */
	if s.low.cats != nil && t.low.cats != nil {
		outrange.low.cats = new(big.Int)
		outrange.low.cats.And(s.low.cats, t.low.cats)
	}
	if s.high.cats != nil && t.high.cats != nil {
		outrange.high.cats = new(big.Int)
		outrange.high.cats.And(s.high.cats, t.high.cats)
	}

	return outrange.String(), nil
}

func readWriteCon(fpath string, val string) (string, error) {
	if fpath == "" {
		return "", ErrEmptyPath
	}
	f, err := os.OpenFile(fpath, os.O_RDWR, 0)
	if err != nil {
		return "", err
	}
	defer f.Close()

	_, err = f.Write([]byte(val))
	if err != nil {
		return "", err
	}

	return readConFd(f)
}

// peerLabel retrieves the label of the client on the other side of a socket
func peerLabel(fd int) (string, error) {
	l, err := unix.GetsockoptString(fd, unix.SOL_SOCKET, unix.SO_PEERSEC)
	if err != nil {
		return "", &os.PathError{Op: "getsockopt", Path: "fd " + strconv.Itoa(fd), Err: err}
	}
	return l, nil
}

// setKeyLabel takes a process label and tells the kernel to assign the
// label to the next kernel keyring that gets created
func setKeyLabel(label string) error {
	// Rather than using /proc/thread-self, we want to use /proc/self to
	// operate on the thread-group leader.
	err := writeConSelf("attr/keycreate", label)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if label == "" && errors.Is(err, os.ErrPermission) {
		return nil
	}
	if errors.Is(err, unix.EACCES) && unix.Getpid() != unix.Gettid() {
		return ErrNotTGLeader
	}
	return err
}

// KeyLabel retrieves the current kernel keyring label setting for this
// thread-group.
func keyLabel() (string, error) {
	// Rather than using /proc/thread-self, we want to use /proc/self to
	// operate on the thread-group leader.
	return readConSelf("attr/keycreate")
}

// get returns the Context as a string
func (c Context) get() string {
	if l := c["level"]; l != "" {
		return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + l
	}
	return c["user"] + ":" + c["role"] + ":" + c["type"]
}

// newContext creates a new Context struct from the specified label
func newContext(label string) (Context, error) {
	c := make(Context)

	if len(label) != 0 {
		con := strings.SplitN(label, ":", 4)
		if len(con) < 3 {
			return c, ErrInvalidLabel
		}
		c["user"] = con[0]
		c["role"] = con[1]
		c["type"] = con[2]
		if len(con) > 3 {
			c["level"] = con[3]
		}
	}
	return c, nil
}

// clearLabels clears all reserved labels
func clearLabels() {
	state.Lock()
	state.mcsList = make(map[string]struct{})
	state.Unlock()
}

// reserveLabel reserves the MLS/MCS level component of the specified label.
func reserveLabel(label string) error {
	if len(label) != 0 {
		con := strings.SplitN(label, ":", 4)
		if len(con) > 3 {
			return mcsAdd(con[3])
		}
	}

	return nil
}

func checkLabel(label string) error {
	if len(label) != 0 {
		con := strings.SplitN(label, ":", 4)
		if len(con) > 3 {
			state.Lock()
			defer state.Unlock()
			if _, exist := state.mcsList[con[3]]; exist {
				return ErrMCSAlreadyExists
			}
		}
	}
	return nil
}

func selinuxEnforcePath() string {
	return filepath.Join(getSelinuxMountPoint(), "enforce")
}

// isMLSEnabled checks if MLS is enabled.
func isMLSEnabled() bool {
	enabledB, err := os.ReadFile(filepath.Join(getSelinuxMountPoint(), "mls"))
	if err != nil {
		return false
	}
	return bytes.Equal(enabledB, []byte{'1'})
}

// enforceMode returns the current SELinux mode Enforcing, Permissive, Disabled
func enforceMode() int {
	var enforce int

	enforceB, err := os.ReadFile(selinuxEnforcePath())
	if err != nil {
		return -1
	}
	enforce, err = strconv.Atoi(string(enforceB))
	if err != nil {
		return -1
	}
	return enforce
}

// setEnforceMode sets the current SELinux mode Enforcing, Permissive.
// Disabled is not valid, since this needs to be set at boot time.
func setEnforceMode(mode int) error {
	return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0)
}

// defaultEnforceMode returns the systems default SELinux mode Enforcing,
// Permissive or Disabled. Note this is just the default at boot time.
// EnforceMode tells you the systems current mode.
func defaultEnforceMode() int {
	switch readConfig(selinuxTag) {
	case "enforcing":
		return Enforcing
	case "permissive":
		return Permissive
	}
	return Disabled
}

// mcsAdd reserves a level. If the argument is empty or does not contain
// MCS/MLS category component (no ":c"), it is ignored.
func mcsAdd(mcs string) error {
	if !strings.Contains(mcs, ":c") {
		return nil
	}
	state.Lock()
	defer state.Unlock()
	if _, exist := state.mcsList[mcs]; exist {
		return ErrMCSAlreadyExists
	}
	state.mcsList[mcs] = struct{}{}
	return nil
}

func mcsDelete(mcs string) {
	if mcs == "" {
		return
	}
	state.Lock()
	defer state.Unlock()
	delete(state.mcsList, mcs)
}

func uniqMcs(catRange uint32) string {
	var (
		c1, c2 uint32
		mcs    string
	)

	for {
		//#nosec G404 -- using slightly more predictable MCS labels won't affect security, so it's fine to use math/rand/v2 here.
		{
			c1 = rand.Uint32N(catRange)
			c2 = rand.Uint32N(catRange)
		}
		if c1 == c2 {
			continue
		} else if c1 > c2 {
			c1, c2 = c2, c1
		}
		mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2)
		if err := mcsAdd(mcs); err != nil {
			continue
		}
		break
	}
	return mcs
}

// releaseLabel un-reserves the MLS/MCS Level field of the specified label,
// allowing it to be used by another process.
func releaseLabel(label string) {
	if len(label) != 0 {
		con := strings.SplitN(label, ":", 4)
		if len(con) > 3 {
			mcsDelete(con[3])
		}
	}
}

// roFileLabel returns the specified SELinux readonly file label
func roFileLabel() string {
	return readOnlyFileLabel
}

func openContextFile() (*os.File, error) {
	if f, err := os.Open(contextFile); err == nil {
		return f, nil
	}
	return os.Open(filepath.Join(policyRoot(), "contexts", "lxc_contexts"))
}

var loadLabels = sync.OnceValue(func() map[string]string {
	labels := make(map[string]string)
	in, err := openContextFile()
	if err != nil {
		return labels
	}
	defer in.Close()

	scanner := bufio.NewScanner(in)

	for scanner.Scan() {
		line := bytes.TrimSpace(scanner.Bytes())
		if len(line) == 0 {
			// Skip blank lines
			continue
		}
		if line[0] == ';' || line[0] == '#' {
			// Skip comments
			continue
		}
		if key, val, ok := bytes.Cut(line, []byte{'='}); ok {
			key, val = bytes.TrimSpace(key), bytes.TrimSpace(val)
			labels[string(key)] = string(bytes.Trim(val, `"`))
		}
	}

	con, _ := NewContext(labels["file"])
	con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1)
	privContainerMountLabel = con.get()
	_ = reserveLabel(privContainerMountLabel)
	return labels
})

func label(key string) string {
	return loadLabels()[key]
}

// kvmContainerLabels returns the default processLabel and mountLabel to be used
// for kvm containers by the calling process.
func kvmContainerLabels() (string, string) {
	processLabel := label("kvm_process")
	if processLabel == "" {
		processLabel = label("process")
	}

	return addMcs(processLabel, label("file"))
}

func kvmContainerLabel() (string, error) {
	processLabel := label("kvm_process")
	if processLabel == "" {
		processLabel = label("process")
	}
	pLabel, _, err := addMcsProc(processLabel)
	return pLabel, err
}

// initContainerLabels returns the default processLabel and file labels to be
// used for containers running an init system like systemd by the calling process.
func initContainerLabels() (string, string) {
	processLabel := label("init_process")
	if processLabel == "" {
		processLabel = label("process")
	}

	return addMcs(processLabel, label("file"))
}

func initContainerLabel() (string, error) {
	processLabel := label("init_process")
	if processLabel == "" {
		processLabel = label("process")
	}

	pLabel, _, err := addMcsProc(processLabel)
	return pLabel, err
}

// containerLabels returns an allocated processLabel and fileLabel to be used for
// container labeling by the calling process.
func containerLabels() (processLabel string, fileLabel string) {
	if !getEnabled() {
		return "", ""
	}

	processLabel = label("process")
	fileLabel = label("file")
	readOnlyFileLabel = label("ro_file")

	if processLabel == "" || fileLabel == "" {
		return "", fileLabel
	}

	if readOnlyFileLabel == "" {
		readOnlyFileLabel = fileLabel
	}

	return addMcs(processLabel, fileLabel)
}

func addMcsProc(processLabel string) (string, string, error) {
	var mcs string
	scon, err := NewContext(processLabel)
	if err != nil {
		return "", "", err
	}
	if scon["level"] != "" {
		mcs = uniqMcs(CategoryRange)
		scon["level"] = mcs
		processLabel = scon.Get()
	}
	return processLabel, mcs, nil
}

func addMcs(processLabel, fileLabel string) (string, string) {
	processLabel, mcs, _ := addMcsProc(processLabel)
	if mcs != "" {
		scon, _ := NewContext(fileLabel)
		scon["level"] = mcs
		fileLabel = scon.Get()
	}
	return processLabel, fileLabel
}

// securityCheckContext validates that the SELinux label is understood by the kernel
func securityCheckContext(val string) error {
	return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0)
}

// copyLevel returns a label with the MLS/MCS level from src label replaced on
// the dest label.
func copyLevel(src, dest string) (string, error) {
	if src == "" {
		return "", nil
	}
	if err := SecurityCheckContext(src); err != nil {
		return "", err
	}
	if err := SecurityCheckContext(dest); err != nil {
		return "", err
	}
	scon, err := NewContext(src)
	if err != nil {
		return "", err
	}
	tcon, err := NewContext(dest)
	if err != nil {
		return "", err
	}
	mcsDelete(tcon["level"])
	_ = mcsAdd(scon["level"])
	tcon["level"] = scon["level"]
	return tcon.Get(), nil
}

// chcon changes the fpath file object to the SELinux label.
// If fpath is a directory and recurse is true, then chcon walks the
// directory tree setting the label.
func chcon(fpath string, label string, recurse bool) error {
	if fpath == "" {
		return ErrEmptyPath
	}
	if label == "" {
		return nil
	}

	excludePaths := map[string]bool{
		"/":           true,
		"/bin":        true,
		"/boot":       true,
		"/dev":        true,
		"/etc":        true,
		"/etc/passwd": true,
		"/etc/pki":    true,
		"/etc/shadow": true,
		"/home":       true,
		"/lib":        true,
		"/lib64":      true,
		"/media":      true,
		"/opt":        true,
		"/proc":       true,
		"/root":       true,
		"/run":        true,
		"/sbin":       true,
		"/srv":        true,
		"/sys":        true,
		"/tmp":        true,
		"/usr":        true,
		"/var":        true,
		"/var/lib":    true,
		"/var/log":    true,
	}

	if home := os.Getenv("HOME"); home != "" {
		excludePaths[home] = true
	}

	if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
		if usr, err := user.Lookup(sudoUser); err == nil {
			excludePaths[usr.HomeDir] = true
		}
	}

	if fpath != "/" {
		fpath = strings.TrimSuffix(fpath, "/")
	}
	if excludePaths[fpath] {
		return fmt.Errorf("SELinux relabeling of %s is not allowed", fpath)
	}

	if !recurse {
		err := lSetFileLabel(fpath, label)
		if err != nil {
			// Check if file doesn't exist, must have been removed
			if errors.Is(err, os.ErrNotExist) {
				return nil
			}
			// Check if current label is correct on disk
			flabel, nerr := lFileLabel(fpath)
			if nerr == nil && flabel == label {
				return nil
			}
			// Check if file doesn't exist, must have been removed
			if errors.Is(nerr, os.ErrNotExist) {
				return nil
			}
			return err
		}
		return nil
	}

	return rchcon(fpath, label)
}

func rchcon(fpath, label string) error { //revive:disable:cognitive-complexity
	fastMode := false
	// If the current label matches the new label, assume
	// other labels are correct.
	if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label {
		fastMode = true
	}
	return pwalkdir.Walk(fpath, func(p string, _ fs.DirEntry, _ error) error {
		if fastMode {
			if cLabel, err := lFileLabel(p); err == nil && cLabel == label {
				return nil
			}
		}
		err := lSetFileLabel(p, label)
		// Walk a file tree can race with removal, so ignore ENOENT.
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		return err
	})
}

// dupSecOpt takes an SELinux process label and returns security options that
// can be used to set the SELinux Type and Level for future container processes.
func dupSecOpt(src string) ([]string, error) {
	if src == "" {
		return nil, nil
	}
	con, err := NewContext(src)
	if err != nil {
		return nil, err
	}
	if con["user"] == "" ||
		con["role"] == "" ||
		con["type"] == "" {
		return nil, nil
	}
	dup := []string{
		"user:" + con["user"],
		"role:" + con["role"],
		"type:" + con["type"],
	}

	if con["level"] != "" {
		dup = append(dup, "level:"+con["level"])
	}

	return dup, nil
}

// checkGroup returns true if group's GID is in the list of GIDs gids.
func checkGroup(group string, gids []string, lookupGroup func(string) (*user.Group, error)) bool {
	grp, err := lookupGroup(group)
	if err != nil {
		return false
	}

	return slices.Contains(gids, grp.Gid)
}

// getSeUserFromReader reads the seusers file: https://www.man7.org/linux/man-pages/man5/seusers.5.html
func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGroup func(string) (*user.Group, error)) (seUser string, level string, err error) {
	var defaultSeUser, defaultLevel string
	var groupSeUser, groupLevel string

	lineNum := -1
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		rawLine := scanner.Text()
		lineNum++

		// remove any trailing comments, then extra whitespace
		line, _, _ := strings.Cut(rawLine, "#")
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		userField, rest, ok := strings.Cut(line, ":")
		if !ok {
			return "", "", fmt.Errorf("line %d: malformed line", lineNum)
		}
		if userField == "" {
			return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum)
		}
		seUserField, rest, ok := strings.Cut(rest, ":")
		if seUserField == "" {
			return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum)
		}
		var levelField string
		// level is optional
		if ok {
			levelField = rest
		}

		// we found a match, return it
		if userField == username {
			return seUserField, levelField, nil
		}

		// if the first field starts with '%' it's a group, check if
		// the user is a member of that group and set the group
		// SELinux user and level if so
		if userField[0] == '%' && groupSeUser == "" {
			if checkGroup(userField[1:], gids, lookupGroup) {
				groupSeUser = seUserField
				groupLevel = levelField
			}
		} else if userField == "__default__" && defaultSeUser == "" {
			defaultSeUser = seUserField
			defaultLevel = levelField
		}
	}
	if err := scanner.Err(); err != nil {
		return "", "", fmt.Errorf("failed to read seusers file: %w", err)
	}

	if groupSeUser != "" {
		return groupSeUser, groupLevel, nil
	}
	if defaultSeUser != "" {
		return defaultSeUser, defaultLevel, nil
	}

	return "", "", fmt.Errorf("could not find SELinux user for %q login", username)
}

// getSeUserByName returns an SELinux user and MLS level that is
// mapped to a given Linux user.
func getSeUserByName(username string) (string, string, error) {
	seUsersConf := filepath.Join(policyRoot(), "seusers")
	confFile, err := os.Open(seUsersConf)
	if err != nil {
		return "", "", fmt.Errorf("failed to open seusers file: %w", err)
	}
	defer confFile.Close()

	usr, err := user.Lookup(username)
	if err != nil {
		return "", "", err
	}
	gids, err := usr.GroupIds()
	if err != nil {
		return "", "", err
	}
	gids = append([]string{usr.Gid}, gids...)

	seUser, level, err := getSeUserFromReader(username, gids, confFile, user.LookupGroup)
	if err != nil {
		return "", "", fmt.Errorf("failed to parse seusers file: %w", err)
	}

	return seUser, level, nil
}

// findUserInContext scans the reader for a valid SELinux context
// match that is verified with the verifier. Invalid contexts are
// skipped. It returns a matched context or an empty string if no
// match is found. If a scanner error occurs, it is returned.
func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
	fromRole := context["role"]
	fromType := context["type"]
	scanner := bufio.NewScanner(r)

	for scanner.Scan() {
		fromConns := strings.Fields(scanner.Text())
		if len(fromConns) == 0 {
			// Skip blank lines
			continue
		}

		line := fromConns[0]

		if line[0] == ';' || line[0] == '#' {
			// Skip comments
			continue
		}

		// user context files contexts are formatted as
		// role_r:type_t:s0 where the user is missing.
		lineArr := strings.SplitN(line, ":", 4)
		// skip context with typo, or role and type do not match
		if len(lineArr) != 3 ||
			lineArr[0] != fromRole ||
			lineArr[1] != fromType {
			continue
		}

		for _, cc := range fromConns[1:] {
			toConns := strings.SplitN(cc, ":", 4)
			if len(toConns) != 3 {
				continue
			}

			context["role"] = toConns[0]
			context["type"] = toConns[1]

			outConn := context.get()
			if err := verifier(outConn); err != nil {
				continue
			}

			return outConn, nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", fmt.Errorf("failed to scan for context: %w", err)
	}

	return "", nil
}

// getFailsafeContext returns the context in the failsafe_context file:
// https://www.man7.org/linux/man-pages/man5/failsafe_context.5.html
func getFailsafeContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
	conn := make([]byte, 256)
	limReader := io.LimitReader(r, int64(len(conn)))
	_, err := limReader.Read(conn)
	if err != nil {
		return "", fmt.Errorf("failed to read failsafe context: %w", err)
	}

	conn = bytes.TrimSpace(conn)
	toConns := strings.SplitN(string(conn), ":", 4)
	if len(toConns) != 3 {
		return "", nil
	}

	context["role"] = toConns[0]
	context["type"] = toConns[1]

	outConn := context.get()
	if err := verifier(outConn); err != nil {
		return "", err
	}

	return outConn, nil
}

func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
	if c.verifier == nil {
		return "", ErrVerifierNil
	}

	context, err := newContext(c.scon)
	if err != nil {
		return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err)
	}

	// set so the verifier validates the matched context with the provided user and level.
	context["user"] = c.user
	context["level"] = c.level

	userRdr, err := c.openUserRdr()
	if err != nil {
		return "", fmt.Errorf("failed to open user context file: %w", err)
	}
	defer userRdr.Close()

	conn, err := findUserInContext(context, userRdr, c.verifier)
	if err != nil {
		return "", fmt.Errorf("failed to read %q's user context file: %w", c.user, err)
	}

	if conn != "" {
		return conn, nil
	}

	defaultRdr, err := c.openDefaultRdr()
	if err != nil {
		return "", fmt.Errorf("failed to open default context file: %w", err)
	}
	defer defaultRdr.Close()

	conn, err = findUserInContext(context, defaultRdr, c.verifier)
	if err != nil {
		return "", fmt.Errorf("failed to read default user context file: %w", err)
	}

	if conn != "" {
		return conn, nil
	}

	failsafeRdr, err := c.openFailsafeRdr()
	if err != nil {
		return "", fmt.Errorf("failed to open failsafe context file: %w", err)
	}
	defer failsafeRdr.Close()

	conn, err = getFailsafeContext(context, failsafeRdr, c.verifier)
	if err != nil {
		return "", fmt.Errorf("failed to read failsafe_context: %w", err)
	}

	if conn != "" {
		return conn, nil
	}

	return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing)
}

func getDefaultContextWithLevel(user, level, scon string) (string, error) {
	userPath := filepath.Join(policyRoot(), selinuxUsersDir, user)
	defaultPath := filepath.Join(policyRoot(), defaultContexts)
	failsafePath := filepath.Join(policyRoot(), failsafeContext)

	c := defaultSECtx{
		user:            user,
		level:           level,
		scon:            scon,
		openUserRdr:     createOpener(userPath),
		openDefaultRdr:  createOpener(defaultPath),
		openFailsafeRdr: createOpener(failsafePath),
		verifier:        securityCheckContext,
	}

	return getDefaultContextFromReaders(&c)
}

func (k ProcessKind) keys() (primary, fallback string, ok bool) {
	switch k {
	case ProcessKindRegular:
		return "process", "", true
	case ProcessKindInit:
		return "init_process", "process", true
	case ProcessKindKVM:
		return "kvm_process", "process", true
	}
	return "", "", false
}

func setProcessKind(cLabel string, k ProcessKind) (string, error) {
	if cLabel == "" {
		return "", nil
	}
	primary, fallback, ok := k.keys()
	if !ok {
		return "", fmt.Errorf("selinux.SetProcessKind: invalid ProcessKind %d", k)
	}

	src := label(primary)
	if src == "" && fallback != "" {
		src = label(fallback)
	}
	if src == "" {
		return cLabel, nil
	}

	// Replace cLabel type with one from src.
	srcCtx, err := newContext(src)
	if err != nil {
		return "", fmt.Errorf("selinux.SetProcessKind: invalid %s label %s: %w", primary, src, err)
	}
	dstCtx, err := newContext(cLabel)
	if err != nil {
		return "", fmt.Errorf("selinux.SetProcessKind: invalid label %s: %w", cLabel, err)
	}

	dstCtx["type"] = srcCtx["type"]
	return dstCtx.get(), nil
}
